feat(PERC-470): Hyperp EMA oracle mode for permissionless markets - #808
Conversation
…arkets
- useCreateMarket: add oracleMode param ('pyth'|'hyperp'|'admin'), use
UpdateHyperpMark (tag 34) instead of KeeperCrank for hyperp markets.
Auto-resolves DEX pool vaults on-chain when not provided.
- /api/oracle/resolve/[ca]: return dexPoolAddress, dexType, and
oracleMode ('hyperp' when supported DEX pool found, no Pyth feed).
- CreateMarketWizard: pass oracleMode + dexPoolAddress to create hook.
Hyperp mode uses index_feed_id=zeros (program reads pool directly).
- useQuickLaunch: auto-detect hyperp_ema when oracle/resolve finds a
PumpSwap/Raydium/Meteora pool. Shows 'DEX pool detected' badge.
Two oracle paths:
1. Pyth-pinned: index_feed_id=pyth_hex, KeeperCrank with Pyth PDA
2. Hyperp EMA: index_feed_id=zeros, UpdateHyperpMark reads DEX pool
with 8-hour EMA + circuit breaker. Fully permissionless.
No keeper dependency for hyperp markets. Anyone can crank.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughAdds Hyperp EMA (permissionless DEX pool) oracle support across oracle resolution, quick-launch, wizard, market-creation hooks, and DB schema; returns dex pool metadata and oracleMode from the resolve endpoint and emits UpdateHyperpMark when creating markets using a DEX pool. Changes
Sequence DiagramsequenceDiagram
participant QuickLaunch as Quick Launch (Oracle Detection)
participant Wizard as Market Wizard (Config)
participant CreateHook as CreateMarket Hook (Wiring)
participant DEX as DEX Pool (Screener / On-chain)
participant Chain as Blockchain (Program)
QuickLaunch->>DEX: fetchDexScreenerInfo / detect DEX pool
DEX-->>QuickLaunch: return dexPoolAddress, dexType, price
QuickLaunch->>Wizard: pass oracleType=hyperp_ema + dexPoolAddress
Wizard->>CreateHook: emit CreateMarketParams (oracleMode=hyperp, dexPoolAddress)
CreateHook->>DEX: read on-chain pool account to get vault addresses
DEX-->>CreateHook: return dexBaseVault, dexQuoteVault
CreateHook->>Chain: emit UpdateHyperpMark instruction (uses pool accounts)
Chain-->>CreateHook: confirm mark update / proceed with market creation
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
dcccrypto
left a comment
There was a problem hiding this comment.
🛡️ Security Review — PR #808 (PERC-470: Hyperp EMA Oracle)
Verdict: CONDITIONAL APPROVAL — 2 issues to fix before merge
MEDIUM — dexPoolAddress not validated as Solana base58 pubkey before use in instruction
File: app/app/api/oracle/resolve/[ca]/route.ts
The pairAddress field from DexScreener is returned as dexPoolAddress directly to the client without any base58/PublicKey validation:
const poolAddress = SUPPORTED_DEX_IDS.has(dexId ?? "") ? (best as any).pairAddress ?? null : null;In useCreateMarket.ts the address is then used in a path without a wrapping try/catch:
{ pubkey: new PublicKey(params.dexPoolAddress), isSigner: false, isWritable: false },A malformed DexScreener response (or MITM) could cause market creation to throw mid-instruction-build with an opaque error, and there is no user-visible failure message.
Fix: Validate in the oracle/resolve route before returning:
import { PublicKey } from '@solana/web3.js';
try { new PublicKey(poolAddress); } catch { poolAddress = null; }OR wrap the instruction-building step in a try/catch in useCreateMarket.ts with a clear error state.
MEDIUM — Program-level pool/mint binding not confirmed in client review
The client passes dexPoolAddress as a remaining account to UpdateHyperpMark (tag 34). The program must validate that the pool's base/quote token matches the market's mint before accepting its reserves as the price source.
Client-side, detectDexType(poolAccount.owner) checks the pool is owned by a known DEX program — that's good. But if the program only checks pool ownership (not token pair match), an attacker could create a legit PumpSwap pool for a different token pair and pass that pool address to manipulate the mark price.
Confirm: Does UpdateHyperpMark in the deployed program (percolator-prog) verify that pool.base_mint == slab.mint (or equivalent)? If yes, this is resolved. If not, file a separate issue for the program.
LOW — Initial mark price defaults to $1 when DEX price unavailable
const dexPrice = wizard.dexPool?.priceUsd ?? 1;If dexPool.priceUsd is null (e.g. DexScreener price fetch failed), the market initialises with a $1 mark price regardless of real value. For high-value or near-zero-value tokens this could create a badly seeded market.
Fix: Gate the create(params) call — refuse to proceed if oracleMode === 'hyperp' and dexPool?.priceUsd is falsy. Show a user error instead.
Clean items ✓
oracle_authority = zerosfor hyperp mode (permissionless, correct)- 8-hour EMA + circuit breaker in program (manipulation-resistant)
- No external API dependency in the hyperp crank path
isDevnetEnvdefaults to mainnet (fail-closed) in CreateMarket hook- DexScreener SUPPORTED_DEX_IDS allowlist (
pumpswap,raydium,meteora) limits attack surface oracle_modeanddex_pool_addresspersisted to DB for auditability
Blocking merge: MEDIUM #1 (easy fix). MEDIUM #2 (needs coder confirmation on program). LOW is non-blocking.
next.config.ts had a catch-all rewrite that proxied /api/oracle/:path*
to Railway BEFORE the new Next.js route.ts could handle it. Railway
returns { bestSource, allSources } format, not { oracleMode, dexPoolAddress }.
Fix: only proxy /api/oracle/publishers to Railway. /api/oracle/resolve/[ca]
now correctly hits the Next.js route that returns oracleMode + dexPoolAddress
for hyperp oracle detection.
Fixes #809
…erp on price - oracle/resolve: validate DexScreener pairAddress with new PublicKey() before returning as dexPoolAddress (MEDIUM #1) - CreateMarketWizard: block hyperp launch if DEX price is 0/unavailable instead of defaulting to $1 (LOW fix) - MEDIUM #2 confirmed: percolator-prog validates DEX pool owner (approved program) + minimum liquidity, but does NOT validate pool.base_mint == slab.collateral_mint. Per source comment: wrong pool yields wrong price but cannot steal funds. Market creator assumes this risk.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/components/create/CreateMarketWizard.tsx (1)
386-402:⚠️ Potential issue | 🟡 Minor
handleRetryis missingoracleModeanddexPoolAddress— may break hyperp market retries.The retry handler constructs
CreateMarketParamswithout includingoracleModeordexPoolAddress, which are required for hyperp markets. If a hyperp market creation fails mid-flow and the user retries, the retry will default to admin mode instead of hyperp.🐛 Proposed fix to include oracleMode and dexPoolAddress in retry
const params: CreateMarketParams = { mint: new PublicKey(effectiveMint), initialPriceE6: priceE6, lpCollateral: parseHumanAmount(wizard.lpCollateral || "0", decimals), insuranceAmount: parseHumanAmount(wizard.insuranceAmount, decimals), oracleFeed, invert: false, tradingFeeBps: wizard.tradingFeeBps, initialMarginBps: wizard.initialMarginBps, maxAccounts: tier.maxAccounts, slabDataSize: tier.dataSize, symbol: wizard.tokenMeta?.symbol ?? "UNKNOWN", name: wizard.tokenMeta?.name ?? "Unknown Token", decimals, mainnetCA: wizard.mintAddress !== effectiveMint ? wizard.mintAddress : undefined, + oracleMode: wizard.oracleType === "pyth" ? "pyth" as const + : wizard.oracleType === "hyperp_ema" ? "hyperp" as const + : "admin" as const, + ...(wizard.oracleType === "hyperp_ema" && wizard.dexPool ? { + dexPoolAddress: wizard.dexPool.poolAddress, + } : {}), };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/create/CreateMarketWizard.tsx` around lines 386 - 402, handleRetry builds CreateMarketParams but omits oracleMode and dexPoolAddress causing hyperp market retries to fall back to admin mode; update the params construction in handleRetry (where create(params, createState.step) is called) to include oracleMode: wizard.oracleMode and dexPoolAddress: wizard.dexPoolAddress (or equivalent properties from wizard) so the same market type and pool address are preserved on retry.
🧹 Nitpick comments (3)
app/app/api/oracle/resolve/[ca]/route.ts (1)
153-176: Consider addingdexIdandpairAddressto the pairs type definition.The current code uses
as anycasts to accessdexIdandpairAddressfields that are returned by the DexScreener API but not declared in the type. This works but reduces type safety.🛠️ Suggested type improvement
const pairs = json.pairs as Array<{ priceUsd?: string; baseToken?: { symbol?: string }; liquidity?: { usd?: number }; chainId?: string; + dexId?: string; + pairAddress?: string; }>;Then remove the
as anycasts:- const dexId = (best as any).dexId?.toLowerCase() ?? null; - const poolAddress = SUPPORTED_DEX_IDS.has(dexId ?? "") ? (best as any).pairAddress ?? null : null; + const dexId = best.dexId?.toLowerCase() ?? null; + const poolAddress = SUPPORTED_DEX_IDS.has(dexId ?? "") ? best.pairAddress ?? null : null;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/app/api/oracle/resolve/`[ca]/route.ts around lines 153 - 176, Update the local pairs type to include dexId?: string and pairAddress?: string so you don't need the unsafe casts; modify the declaration where pairs is defined (the inline type in route.ts) to add those optional fields, then remove the two (best as any).dexId and (best as any).pairAddress casts and use best.dexId?.toLowerCase() and best.pairAddress directly (keeping the existing SUPPORTED_DEX_IDS check and null coalescing).app/hooks/useCreateMarket.ts (2)
227-242: Consider extracting DEX pool resolution to avoid direct parameter mutation.The code mutates
params.dexBaseVaultandparams.dexQuoteVaultdirectly. While functionally correct (params is only used within this function), this pattern can be surprising. A minor refactor could use local variables instead.♻️ Alternative approach with local variables
+ let resolvedBaseVault = params.dexBaseVault; + let resolvedQuoteVault = params.dexQuoteVault; + if (isHyperpOracle && params.dexPoolAddress && !params.dexBaseVault) { try { const poolPk = new PublicKey(params.dexPoolAddress); const poolAccount = await connection.getAccountInfo(poolPk); if (poolAccount?.data) { const dexType = detectDexType(poolAccount.owner); if (dexType) { const poolInfo = parseDexPool(dexType, poolPk, poolAccount.data); - if (poolInfo.baseVault) params.dexBaseVault = poolInfo.baseVault.toBase58(); - if (poolInfo.quoteVault) params.dexQuoteVault = poolInfo.quoteVault.toBase58(); + if (poolInfo.baseVault) resolvedBaseVault = poolInfo.baseVault.toBase58(); + if (poolInfo.quoteVault) resolvedQuoteVault = poolInfo.quoteVault.toBase58(); } } } catch (e) { console.warn("PERC-470: Failed to resolve DEX pool vaults:", e); } }Then use
resolvedBaseVault/resolvedQuoteVaultin the instruction building below.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/hooks/useCreateMarket.ts` around lines 227 - 242, The code currently mutates params.dexBaseVault and params.dexQuoteVault inside the DEX pool resolution block (when isHyperpOracle && params.dexPoolAddress && !params.dexBaseVault); instead, introduce local variables (e.g., resolvedBaseVault and resolvedQuoteVault) and assign parseDexPool(...) results to those locals without modifying params directly, then use these locals when building the instruction later; keep the existing logic and error handling (PublicKey, connection.getAccountInfo, detectDexType, parseDexPool) but replace direct writes to params.dexBaseVault / params.dexQuoteVault with assignments to the new local variables and use those locals downstream.
589-614: Consider extracting UpdateHyperpMark instruction building to reduce duplication.The UpdateHyperpMark instruction construction (lines 589-614 and 774-796) is nearly identical. Extracting to a helper function would improve maintainability.
♻️ Proposed helper function
function buildUpdateHyperpMarkIx( programId: PublicKey, slabPk: PublicKey, dexPoolAddress: string, dexBaseVault?: string, dexQuoteVault?: string, ): TransactionInstruction { const hyperpData = encodeUpdateHyperpMark(); const hyperpKeys: { pubkey: PublicKey; isSigner: boolean; isWritable: boolean }[] = [ { pubkey: slabPk, isSigner: false, isWritable: true }, { pubkey: new PublicKey(dexPoolAddress), isSigner: false, isWritable: false }, { pubkey: WELL_KNOWN.clock, isSigner: false, isWritable: false }, ]; if (dexBaseVault) { hyperpKeys.push({ pubkey: new PublicKey(dexBaseVault), isSigner: false, isWritable: false }); } if (dexQuoteVault) { hyperpKeys.push({ pubkey: new PublicKey(dexQuoteVault), isSigner: false, isWritable: false }); } return new TransactionInstruction({ programId, keys: hyperpKeys, data: Buffer.from(hyperpData) }); }Then use:
- const hyperpData = encodeUpdateHyperpMark(); - const hyperpKeys: { pubkey: PublicKey; isSigner: boolean; isWritable: boolean }[] = [ - { pubkey: slabPk, isSigner: false, isWritable: true }, - { pubkey: new PublicKey(params.dexPoolAddress), isSigner: false, isWritable: false }, - { pubkey: WELL_KNOWN.clock, isSigner: false, isWritable: false }, - ]; - if (params.dexBaseVault) { - hyperpKeys.push({ pubkey: new PublicKey(params.dexBaseVault), isSigner: false, isWritable: false }); - } - if (params.dexQuoteVault) { - hyperpKeys.push({ pubkey: new PublicKey(params.dexQuoteVault), isSigner: false, isWritable: false }); - } - instructions.push(new TransactionInstruction({ programId, keys: hyperpKeys, data: Buffer.from(hyperpData) })); + instructions.push(buildUpdateHyperpMarkIx(programId, slabPk, params.dexPoolAddress, params.dexBaseVault, params.dexQuoteVault));Also applies to: 774-796
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/hooks/useCreateMarket.ts` around lines 589 - 614, The UpdateHyperpMark instruction construction is duplicated (blocks using encodeUpdateHyperpMark, WELL_KNOWN, slabPk, params.dexPoolAddress, params.dexBaseVault, params.dexQuoteVault and new TransactionInstruction) — extract that logic into a helper (e.g., buildUpdateHyperpMarkIx) that accepts programId, slabPk, dexPoolAddress, dexBaseVault?, dexQuoteVault? and returns a TransactionInstruction built from encodeUpdateHyperpMark and the assembled keys; then replace both inline blocks with calls to buildUpdateHyperpMarkIx to remove duplication and keep behavior identical.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/hooks/useCreateMarket.ts`:
- Around line 869-870: The POST to /api/markets from useCreateMarket.ts is
sending fields oracle_mode and dex_pool_address but will fail because the API
currently rejects write methods (see packages/api/src/index.ts global 405) and
the markets table lacks those columns; fix this by (1) adding POST handler for
/api/markets in the API router that validates and accepts create payloads (allow
POST in packages/api/src/index.ts or implement a dedicated route/controller) and
(2) adding a DB migration to add oracle_mode and dex_pool_address columns to the
markets table and update any ORM/schema (so the create logic in
useCreateMarket.ts aligns with the DB schema). Ensure request validation/schema
for the POST endpoint matches the fields sent by useCreateMarket.ts
(oracle_mode, dex_pool_address).
---
Outside diff comments:
In `@app/components/create/CreateMarketWizard.tsx`:
- Around line 386-402: handleRetry builds CreateMarketParams but omits
oracleMode and dexPoolAddress causing hyperp market retries to fall back to
admin mode; update the params construction in handleRetry (where create(params,
createState.step) is called) to include oracleMode: wizard.oracleMode and
dexPoolAddress: wizard.dexPoolAddress (or equivalent properties from wizard) so
the same market type and pool address are preserved on retry.
---
Nitpick comments:
In `@app/app/api/oracle/resolve/`[ca]/route.ts:
- Around line 153-176: Update the local pairs type to include dexId?: string and
pairAddress?: string so you don't need the unsafe casts; modify the declaration
where pairs is defined (the inline type in route.ts) to add those optional
fields, then remove the two (best as any).dexId and (best as any).pairAddress
casts and use best.dexId?.toLowerCase() and best.pairAddress directly (keeping
the existing SUPPORTED_DEX_IDS check and null coalescing).
In `@app/hooks/useCreateMarket.ts`:
- Around line 227-242: The code currently mutates params.dexBaseVault and
params.dexQuoteVault inside the DEX pool resolution block (when isHyperpOracle
&& params.dexPoolAddress && !params.dexBaseVault); instead, introduce local
variables (e.g., resolvedBaseVault and resolvedQuoteVault) and assign
parseDexPool(...) results to those locals without modifying params directly,
then use these locals when building the instruction later; keep the existing
logic and error handling (PublicKey, connection.getAccountInfo, detectDexType,
parseDexPool) but replace direct writes to params.dexBaseVault /
params.dexQuoteVault with assignments to the new local variables and use those
locals downstream.
- Around line 589-614: The UpdateHyperpMark instruction construction is
duplicated (blocks using encodeUpdateHyperpMark, WELL_KNOWN, slabPk,
params.dexPoolAddress, params.dexBaseVault, params.dexQuoteVault and new
TransactionInstruction) — extract that logic into a helper (e.g.,
buildUpdateHyperpMarkIx) that accepts programId, slabPk, dexPoolAddress,
dexBaseVault?, dexQuoteVault? and returns a TransactionInstruction built from
encodeUpdateHyperpMark and the assembled keys; then replace both inline blocks
with calls to buildUpdateHyperpMarkIx to remove duplication and keep behavior
identical.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 9be7fe0b-7126-4502-8743-c837ed28f28c
📒 Files selected for processing (5)
app/app/api/oracle/resolve/[ca]/route.tsapp/components/create/CreateMarketWizard.tsxapp/hooks/useCreateMarket.tsapp/hooks/useQuickLaunch.tsapp/next.config.ts
| oracle_mode: oracleMode, | ||
| dex_pool_address: params.dexPoolAddress ?? null, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if migrations exist for the new columns
fd -e sql . supabase | xargs grep -l -E 'oracle_mode|dex_pool_address' 2>/dev/null || echo "No migration files found containing oracle_mode or dex_pool_address"
# Also check the API handler to see how it handles unknown columns
rg -n 'oracle_mode|dex_pool_address' --type ts -g '!*.test.*' app/app/api/marketsRepository: dcccrypto/percolator-launch
Length of output: 138
🏁 Script executed:
# Find the schema file
find . -name "schema.sql" -o -name "*schema*" | head -20
# Also search for where the markets table is defined
rg -n "CREATE TABLE.*markets" --type sqlRepository: dcccrypto/percolator-launch
Length of output: 2254
🏁 Script executed:
# Find the API handler for markets
find . -path "*/api/*" -name "*market*" -type f | head -20
# Also search for the useCreateMarket hook to understand the data flow
rg -n "INSERT INTO markets" --type ts --type jsRepository: dcccrypto/percolator-launch
Length of output: 151
🏁 Script executed:
# Check the markets table definition in the schema
rg -A 30 "CREATE TABLE.*markets" supabase/schema.sql
# Check migration files for oracle_mode and dex_pool_address
cat supabase/migrations/001_initial_schema.sql | head -100Repository: dcccrypto/percolator-launch
Length of output: 3501
🏁 Script executed:
# Check the API handler
cat packages/api/src/routes/markets.tsRepository: dcccrypto/percolator-launch
Length of output: 4774
🏁 Script executed:
# Check the other migration file that might add these columns
cat supabase/migrations/005_market_stats_schema_update.sql
# List all migration files to see what migrations exist
ls -la supabase/migrations/Repository: dcccrypto/percolator-launch
Length of output: 4223
🏁 Script executed:
# Find and read the useCreateMarket hook
cat app/hooks/useCreateMarket.tsRepository: dcccrypto/percolator-launch
Length of output: 43277
🏁 Script executed:
# Search for POST handler for /api/markets
rg -n "POST|/api/markets" packages/api --type ts -A 5 -B 2 | head -100
# Search for any handler that processes market creation
fd -e ts . packages/api | xargs rg -l "oracle_mode|dex_pool_address"
# Check if there are migrations that add these columns to markets table
cat supabase/migrations/*.sql | grep -i -E "ALTER TABLE markets|oracle_mode|dex_pool_address"Repository: dcccrypto/percolator-launch
Length of output: 7684
🏁 Script executed:
# Check if there's a separate API handler file
find packages/api -name "*market*" -type f
# Check the API structure
ls -la packages/api/src/Repository: dcccrypto/percolator-launch
Length of output: 432
🏁 Script executed:
# Read the API index.ts to see the POST rejection logic
cat packages/api/src/index.ts
# Also search for any POST route definitions
rg -n "\.post\(" packages/api/src/routes/Repository: dcccrypto/percolator-launch
Length of output: 7882
POST request to /api/markets will fail: both endpoint rejection and schema mismatch.
The oracle_mode and dex_pool_address fields being sent in the POST request (lines 869-870) have two blocking issues:
-
Missing database columns: The
marketstable schema does not includeoracle_modeordex_pool_addresscolumns. A database migration is required to add them. -
No POST endpoint implemented: The API (packages/api/src/index.ts) globally rejects all POST/PUT/DELETE/PATCH requests with a 405 "Method not allowed" response. The comment in the code explicitly states: "Until write endpoints are added, reject any POST/PUT/DELETE/PATCH requests." The
/api/marketsendpoint currently only supports GET requests.
Both the endpoint implementation and database schema updates are required before market creation can succeed.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/hooks/useCreateMarket.ts` around lines 869 - 870, The POST to
/api/markets from useCreateMarket.ts is sending fields oracle_mode and
dex_pool_address but will fail because the API currently rejects write methods
(see packages/api/src/index.ts global 405) and the markets table lacks those
columns; fix this by (1) adding POST handler for /api/markets in the API router
that validates and accepts create payloads (allow POST in
packages/api/src/index.ts or implement a dedicated route/controller) and (2)
adding a DB migration to add oracle_mode and dex_pool_address columns to the
markets table and update any ORM/schema (so the create logic in
useCreateMarket.ts aligns with the DB schema). Ensure request validation/schema
for the POST endpoint matches the fields sent by useCreateMarket.ts
(oracle_mode, dex_pool_address).
QA Review — PR #808 (PERC-470) — 🔴 Changes RequiredTests Run
Bug #809 Fix — VERIFIED ✅
// PumpSwap token (8PzFWy...pump):
{ "oracleMode": "hyperp", "dexPoolAddress": "Ebs3mXAzqZfzHfsdinTNw7gPy4uNyEAywcCiJxzLRrBW", "price": 0.000878 }
// SOL:
{ "oracleMode": "pyth", "feedId": "ef0d8b...", "price": 84.62 }
// Invalid CA:
HTTP 400 { "error": "Invalid Solana mint address" }MEDIUM #1 Security Fix — VERIFIED ✅
UI Flow — VERIFIED ✅Entering PumpSwap CA shows ✓ DEX pool detected badge, Quick Launch auto-advances through oracle step. Pyth tokens still route to Pyth mode correctly. 🔴 Blocking Bugs — Must Fix Before Merge#810 — High: handleRetry loses oracleMode + dexPoolAddress #811 — Medium: oracle_mode + dex_pool_address not persisted to DB Both bugs were also independently flagged by CodeRabbit. Please fix and ping for re-review. |
…, #811) Bug #810: handleRetry in CreateMarketWizard.tsx was missing oracleMode and dexPoolAddress params, causing hyperp retries to silently fall back to admin mode. Fixed by computing oracleMode from wizard state. Bug #811: oracle_mode and dex_pool_address weren't stored in the markets table. Added migration 035 (ALTER TABLE + index) and updated the POST /api/markets route to insert both fields. Fixes #810, #811
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/app/api/oracle/resolve/[ca]/route.ts (1)
161-180:⚠️ Potential issue | 🟠 MajorSearch for the best supported pool, not just the most liquid pair overall.
This picks the top Solana pair by liquidity first and only then checks whether its
dexIdis supported. If the deepest pair is on an unsupported venue,poolAddressis nulled out even when a valid PumpSwap/Raydium/Meteora pool is present lower in the list, so Hyperp auto-detection incorrectly falls back to admin mode.Suggested fix
- const best = solPairs[0]; + const best = solPairs[0]; + const supportedPairs = solPairs.filter((p) => + SUPPORTED_DEX_IDS.has((p as any).dexId?.toLowerCase() ?? "") + ); + const bestSupported = supportedPairs[0] ?? null; const price = parseFloat(best.priceUsd ?? "0"); if (!isFinite(price) || price <= 0) return null; @@ - const dexId = (best as any).dexId?.toLowerCase() ?? null; - let poolAddress: string | null = SUPPORTED_DEX_IDS.has(dexId ?? "") ? (best as any).pairAddress ?? null : null; + const dexId = bestSupported ? (bestSupported as any).dexId?.toLowerCase() ?? null : null; + let poolAddress: string | null = bestSupported ? (bestSupported as any).pairAddress ?? null : null;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/app/api/oracle/resolve/`[ca]/route.ts around lines 161 - 180, The code currently picks the top Solana pair by liquidity (solPairs[0]) then checks if its dexId is supported, which can skip a supported pool further down; instead filter or search solPairs for the highest-liquidity entry whose dexId (normalize via .toLowerCase()) is in SUPPORTED_DEX_IDS and has a non-null pairAddress, validate that pairAddress with new PublicKey(...) before accepting it, and only then compute price/symbol/dexId/poolAddress from that selected entry (fall back to returning null if no supported, valid pool is found). Use the existing symbols: solPairs, SUPPORTED_DEX_IDS, dexId, poolAddress, best, PublicKey to locate and implement this change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/app/api/markets/route.ts`:
- Around line 69-70: The endpoint currently accepts any oracle_mode and
dex_pool_address (and treats falsy oracle_mode as "admin"); add strict
validation in the API handler that reads oracle_mode and dex_pool_address:
accept only a known set of modes (e.g., "admin", "hyperp", "other_allowed_mode")
and return a 400 on unknown or missing oracle_mode, and when oracle_mode ===
"hyperp" validate dex_pool_address is a well-formed Solana public key (base58
length/format check) and return 400 if invalid; update the same validation logic
for both occurrences that set oracle_mode/dex_pool_address so unknown modes are
rejected and hyperp requires a valid pubkey before persisting.
In `@app/components/create/CreateMarketWizard.tsx`:
- Around line 221-228: The Quick Launch branch in CreateMarketWizard sets
oracleFeed from quickLaunch.dexPoolAddress but leaves dexPool null when
quickLaunch.poolInfo is null, causing downstream create paths that check
wizard.dexPool to drop the pool address; update the quick-launch mapping (the
block that returns {...base, oracleType: "hyperp_ema", oracleFeed:
quickLaunch.dexPoolAddress, dexPool: quickLaunch.poolInfo ?? null}) so that
dexPool contains the dexPoolAddress when poolInfo is missing (e.g. set dexPool
to quickLaunch.poolInfo ?? quickLaunch.dexPoolAddress or otherwise ensure
wizard.dexPool or a dedicated wizard.dexPoolAddress field is populated), and
make the create/retry paths that depend on wizard.dexPool use this value (refer
to CreateMarketWizard's quickLaunch, wizard, oracleFeed, dexPool and the return
mapping) so resolved Hyperp launches never lose the pool address.
In `@supabase/migrations/035_add_oracle_mode_to_markets.sql`:
- Around line 5-10: Update the DB schema snapshot and generated TypeScript types
to match the migration: add the new columns oracle_mode (TEXT, default 'admin')
and dex_pool_address to the schema snapshot (supabase/schema.sql) and regenerate
or manually update the Supabase types in app/lib/database.types.ts so both the
markets table type includes oracle_mode and dex_pool_address (with appropriate
types/nullable settings) and any relevant index metadata is reflected where
needed.
---
Outside diff comments:
In `@app/app/api/oracle/resolve/`[ca]/route.ts:
- Around line 161-180: The code currently picks the top Solana pair by liquidity
(solPairs[0]) then checks if its dexId is supported, which can skip a supported
pool further down; instead filter or search solPairs for the highest-liquidity
entry whose dexId (normalize via .toLowerCase()) is in SUPPORTED_DEX_IDS and has
a non-null pairAddress, validate that pairAddress with new PublicKey(...) before
accepting it, and only then compute price/symbol/dexId/poolAddress from that
selected entry (fall back to returning null if no supported, valid pool is
found). Use the existing symbols: solPairs, SUPPORTED_DEX_IDS, dexId,
poolAddress, best, PublicKey to locate and implement this change.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 9964b88b-e80c-4856-b2b8-917179d3cc08
📒 Files selected for processing (4)
app/app/api/markets/route.tsapp/app/api/oracle/resolve/[ca]/route.tsapp/components/create/CreateMarketWizard.tsxsupabase/migrations/035_add_oracle_mode_to_markets.sql
| oracle_mode, | ||
| dex_pool_address, |
There was a problem hiding this comment.
Validate oracle_mode and dex_pool_address before persisting them.
Right now any slab admin can store arbitrary oracle_mode text and any dex_pool_address string through this public endpoint, and falsy oracle_mode values silently downgrade to "admin". That can poison downstream hyperp/admin routing and keeper queries. Please reject unknown modes and require a valid Solana pubkey when oracle_mode === "hyperp".
Suggested validation
+ const allowedOracleModes = new Set(["pyth", "hyperp", "admin"]);
+ if (oracle_mode != null && !allowedOracleModes.has(oracle_mode)) {
+ return NextResponse.json({ error: "Invalid oracle_mode" }, { status: 400 });
+ }
+
+ if (oracle_mode === "hyperp") {
+ if (!dex_pool_address) {
+ return NextResponse.json({ error: "dex_pool_address is required for hyperp markets" }, { status: 400 });
+ }
+ try {
+ new PublicKey(dex_pool_address);
+ } catch {
+ return NextResponse.json({ error: "Invalid dex_pool_address" }, { status: 400 });
+ }
+ }
+
const { data: market, error: marketError } = await (supabase
.from("markets") as any)
.insert({
@@
- oracle_mode: oracle_mode || "admin",
- dex_pool_address: dex_pool_address || null,
+ oracle_mode: oracle_mode ?? "admin",
+ dex_pool_address: dex_pool_address ?? null,
})Also applies to: 133-134
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/app/api/markets/route.ts` around lines 69 - 70, The endpoint currently
accepts any oracle_mode and dex_pool_address (and treats falsy oracle_mode as
"admin"); add strict validation in the API handler that reads oracle_mode and
dex_pool_address: accept only a known set of modes (e.g., "admin", "hyperp",
"other_allowed_mode") and return a 400 on unknown or missing oracle_mode, and
when oracle_mode === "hyperp" validate dex_pool_address is a well-formed Solana
public key (base58 length/format check) and return 400 if invalid; update the
same validation logic for both occurrences that set oracle_mode/dex_pool_address
so unknown modes are rejected and hyperp requires a valid pubkey before
persisting.
| if (quickLaunch.oracleType === "hyperp_ema" && quickLaunch.dexPoolAddress) { | ||
| return { | ||
| ...base, | ||
| oracleType: "hyperp_ema" as const, | ||
| oracleFeed: quickLaunch.dexPoolAddress, | ||
| adminPrice: quickLaunch.adminPrice, | ||
| dexPool: quickLaunch.poolInfo ?? null, | ||
| }; |
There was a problem hiding this comment.
Don’t make Hyperp launch depend on poolInfo being present.
Quick Launch resolves Hyperp from /api/oracle/resolve via dexPoolAddress, but this path only copies that address into wizard.oracleFeed. If quickLaunch.poolInfo is null here, both create paths omit dexPoolAddress because they gate on wizard.dexPool, so a resolved Hyperp market can still lose its pool address on launch/retry.
Suggested direction
interface WizardState {
mode: "quick" | "manual";
step: WizardStep;
@@
oracleType: "pyth" | "hyperp_ema" | "admin";
oracleFeed: string;
+ dexPoolAddress: string | null;
dexPool: DexPoolResult | null;
@@
oracleType: "admin",
oracleFeed: "",
+ dexPoolAddress: null,
dexPool: null,
@@
if (quickLaunch.oracleType === "hyperp_ema" && quickLaunch.dexPoolAddress) {
return {
...base,
oracleType: "hyperp_ema" as const,
oracleFeed: quickLaunch.dexPoolAddress,
+ dexPoolAddress: quickLaunch.dexPoolAddress,
adminPrice: quickLaunch.adminPrice,
dexPool: quickLaunch.poolInfo ?? null,
};
}
@@
- ...(oracleMode === "hyperp" && wizard.dexPool ? {
- dexPoolAddress: wizard.dexPool.poolAddress,
+ ...(oracleMode === "hyperp" && (wizard.dexPoolAddress || wizard.oracleFeed) ? {
+ dexPoolAddress: wizard.dexPoolAddress ?? wizard.oracleFeed,
} : {}),Also applies to: 381-383, 416-418
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/components/create/CreateMarketWizard.tsx` around lines 221 - 228, The
Quick Launch branch in CreateMarketWizard sets oracleFeed from
quickLaunch.dexPoolAddress but leaves dexPool null when quickLaunch.poolInfo is
null, causing downstream create paths that check wizard.dexPool to drop the pool
address; update the quick-launch mapping (the block that returns {...base,
oracleType: "hyperp_ema", oracleFeed: quickLaunch.dexPoolAddress, dexPool:
quickLaunch.poolInfo ?? null}) so that dexPool contains the dexPoolAddress when
poolInfo is missing (e.g. set dexPool to quickLaunch.poolInfo ??
quickLaunch.dexPoolAddress or otherwise ensure wizard.dexPool or a dedicated
wizard.dexPoolAddress field is populated), and make the create/retry paths that
depend on wizard.dexPool use this value (refer to CreateMarketWizard's
quickLaunch, wizard, oracleFeed, dexPool and the return mapping) so resolved
Hyperp launches never lose the pool address.
| ALTER TABLE markets | ||
| ADD COLUMN IF NOT EXISTS oracle_mode TEXT NOT NULL DEFAULT 'admin', | ||
| ADD COLUMN IF NOT EXISTS dex_pool_address TEXT; | ||
|
|
||
| -- Index for filtering hyperp markets (keeper/crank queries) | ||
| CREATE INDEX IF NOT EXISTS idx_markets_oracle_mode ON markets (oracle_mode); |
There was a problem hiding this comment.
Sync the schema snapshot and generated Supabase types with this migration.
This adds oracle_mode and dex_pool_address, but supabase/schema.sql and app/lib/database.types.ts still omit both fields. Fresh DBs created from the snapshot and any typed DB access will stay out of sync with migrated environments.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@supabase/migrations/035_add_oracle_mode_to_markets.sql` around lines 5 - 10,
Update the DB schema snapshot and generated TypeScript types to match the
migration: add the new columns oracle_mode (TEXT, default 'admin') and
dex_pool_address to the schema snapshot (supabase/schema.sql) and regenerate or
manually update the Supabase types in app/lib/database.types.ts so both the
markets table type includes oracle_mode and dex_pool_address (with appropriate
types/nullable settings) and any relevant index metadata is reflected where
needed.
QA Sign-off ✅ — PERC-470 PR #808Re-test after bugs #809, #810, #811 fixes CI: 13/13 ✅Bug #810 — handleRetry oracleMode+dexPoolAddress: ✅ FIXED
Bug #811 — oracle_mode/dex_pool_address DB columns: ✅ FIXEDMigration Bug #809 — Railway rewrite: ✅ FIXED
Vercel preview API tests:
QA-APPROVED. Safe to merge. |
dcccrypto
left a comment
There was a problem hiding this comment.
🛡️ Security Review — PERC-470 PR #808 — CLEARED ✅
Previously Raised Findings
All three addressed and verified:
MEDIUM #1 ✅ FIXED — oracle/resolve route validates poolAddress with new PublicKey() before returning. Invalid addresses → null. Client falls through to admin oracle mode.
MEDIUM #2 ✅ CONFIRMED SAFE — KeeperCrank path (PumpSwap): read_pumpswap_price_e6 asserts pool.key == config.index_feed_id on every crank. Pool substitution rejected on-chain. UpdateHyperpMark additionally hardened with base_mint == collateral_mint guard (percolator-prog, separate branch) — defence-in-depth.
LOW ✅ FIXED — priceE6 === 0n guard in handleLaunch blocks hyperp market creation without a valid DEX price.
New Finding — Non-Blocking
LOW #812 — /api/markets POST accepts oracle_mode and dex_pool_address from client without server-side validation (no allowlist check, no pubkey parse). On-chain security unaffected — program validates independently. Track in #812 and fix before mainnet.
Architecture — OK
oracle_authoritystays zeros for hyperp (permissionless) ✅- No SetOracleAuthority delegation for hyperp ✅
- Vault fetch failure is graceful (warn, no crash) ✅
- DB migration safe (NOT NULL DEFAULT 'admin') ✅
- DexScreener pool candidates scoped to SUPPORTED_DEX_IDS allowlist ✅
PR #808 is cleared for merge. Fix #812 as follow-up before mainnet.
QA Test Results: PERC-470 ✅Preview tested: percolator-launch-git-feature-pe-cbbdf3-khubair-nasirs-projects.vercel.app Bug Fix Verification#809 (Railway rewrite clobbering /api/oracle/resolve) ✅
#810 (handleRetry missing oracleMode/dexPoolAddress) ✅
#811 (migration 035) ✅
Oracle Mode Routing
CI: All 9 checks green ✅Cannot self-approve — notifying PM for merge approval. |
dcccrypto
left a comment
There was a problem hiding this comment.
🛡️ Security Review — PERC-470 (Hyperp EMA Oracle Mode)
SECURITY CLEARED — all previously filed issues resolved. Two new LOWs noted below.
Previously Filed Findings — Status
| Finding | Severity | Status |
|---|---|---|
| MEDIUM #1: dexPoolAddress unvalidated before return | MEDIUM | ✅ FIXED — PublicKey() validation in fetchDexScreenerInfo |
| MEDIUM #2: Pool base_mint not checked vs collateral_mint | MEDIUM | ✅ CONFIRMED SAFE — KeeperCrank path validates pool.key == index_feed_id on-chain. Defense-in-depth fix committed separately on fix/kani-nightly-harness-filter. |
| LOW: priceE6=0n default fallback in hyperp mode | LOW | ✅ FIXED — handleLaunch blocks with user-facing alert if priceE6 === 0n |
New LOW Findings (non-blocking)
LOW-1: oracle_mode not validated server-side — app/api/markets/route.ts accepts oracle_mode as raw string with no enum check. Supabase migration has no CHECK constraint. Recommend: validate against ['pyth','hyperp','admin'] in route + CHECK constraint in migration.
LOW-2: dex_pool_address not validated server-side — malformed pubkey strings can be persisted via crafted POST. Recommend: PublicKey() validation in route.ts before insert.
NOTE: next.config.ts routing narrowing — general /api/oracle/:path* Railway proxy replaced with only /api/oracle/publishers. Verify no other oracle/* routes still need proxying.
Architecture Assessment
- oracle_authority stays zeros for hyperp ✅
- On-chain program validates pool owner = approved DEX program ✅
- Vault address resolution via on-chain getAccountInfo(), not client-supplied ✅
- Permissionless flow correct — no keeper delegation for hyperp ✅
Cleared for merge. File issues for LOW-1 and LOW-2 as follow-up.
* feat(PERC-470): implement Hyperp EMA oracle mode for permissionless markets
- useCreateMarket: add oracleMode param ('pyth'|'hyperp'|'admin'), use
UpdateHyperpMark (tag 34) instead of KeeperCrank for hyperp markets.
Auto-resolves DEX pool vaults on-chain when not provided.
- /api/oracle/resolve/[ca]: return dexPoolAddress, dexType, and
oracleMode ('hyperp' when supported DEX pool found, no Pyth feed).
- CreateMarketWizard: pass oracleMode + dexPoolAddress to create hook.
Hyperp mode uses index_feed_id=zeros (program reads pool directly).
- useQuickLaunch: auto-detect hyperp_ema when oracle/resolve finds a
PumpSwap/Raydium/Meteora pool. Shows 'DEX pool detected' badge.
Two oracle paths:
1. Pyth-pinned: index_feed_id=pyth_hex, KeeperCrank with Pyth PDA
2. Hyperp EMA: index_feed_id=zeros, UpdateHyperpMark reads DEX pool
with 8-hour EMA + circuit breaker. Fully permissionless.
No keeper dependency for hyperp markets. Anyone can crank.
* fix(PERC-470): exclude /api/oracle/resolve from Railway rewrite
next.config.ts had a catch-all rewrite that proxied /api/oracle/:path*
to Railway BEFORE the new Next.js route.ts could handle it. Railway
returns { bestSource, allSources } format, not { oracleMode, dexPoolAddress }.
Fix: only proxy /api/oracle/publishers to Railway. /api/oracle/resolve/[ca]
now correctly hits the Next.js route that returns oracleMode + dexPoolAddress
for hyperp oracle detection.
Fixes #809
* fix(PERC-470): security hardening — validate dexPoolAddress, gate hyperp on price
- oracle/resolve: validate DexScreener pairAddress with new PublicKey()
before returning as dexPoolAddress (MEDIUM #1)
- CreateMarketWizard: block hyperp launch if DEX price is 0/unavailable
instead of defaulting to $1 (LOW fix)
- MEDIUM #2 confirmed: percolator-prog validates DEX pool owner (approved
program) + minimum liquidity, but does NOT validate pool.base_mint ==
slab.collateral_mint. Per source comment: wrong pool yields wrong price
but cannot steal funds. Market creator assumes this risk.
* fix(PERC-470): retry preserves oracleMode + DB stores oracle_mode (#810, #811)
Bug #810: handleRetry in CreateMarketWizard.tsx was missing oracleMode
and dexPoolAddress params, causing hyperp retries to silently fall back
to admin mode. Fixed by computing oracleMode from wizard state.
Bug #811: oracle_mode and dex_pool_address weren't stored in the markets
table. Added migration 035 (ALTER TABLE + index) and updated the POST
/api/markets route to insert both fields.
Fixes #810, #811
---------
Co-authored-by: dcccrypto <dcccrypto@users.noreply.github.com>
Summary
Implements the Hyperp EMA oracle mode — permissionless on-chain pricing via DEX pools (PumpSwap, Raydium CLMM, Meteora DLMM). No keeper dependency.
Two Oracle Paths
Changes
useCreateMarket.tsoracleModeparam:'pyth' | 'hyperp' | 'admin'dexPoolAddress,dexBaseVault,dexQuoteVaultparamsUpdateHyperpMarkinstead ofKeeperCrankin both pre-LP and post-LP cranksdetectDexType+parseDexPoolfrom SDK/api/oracle/resolve/[ca]dexPoolAddress,dexType, andoracleModein responseoracleMode: 'hyperp'CreateMarketWizard.tsxoracleMode+dexPoolAddressto create hookindexFeedId = zeros(program reads pool directly)useQuickLaunch.tshyperp_emawhen oracle/resolve finds a supported DEX poolHow to Test
Tests
Closes PERC-470
Summary by CodeRabbit
New Features
Bug Fixes
Chores