/** * РЕШАЮЩАЯ проверка: цена закрытия берётся из истории на момент экспирации, * а НЕ из текущей цены в момент клика. * * Почему прошлые прогоны были слабыми: сравнивали exitPrice с ценой момента клика, * а цена случайно стояла на месте — сравнение вырождалось. * * Здесь ждём момента, когда цена на экспирации и текущая цена РАЗЛИЧАЮТСЯ, * и только тогда закрываем. Тогда исход однозначен: * - уязвимое поведение -> exitPrice == текущая цена (момент клика) * - исправленное -> exitPrice == цена на момент экспирации */ const { Connection, PublicKey } = require("@solana/web3.js"); const { DatabaseSync } = require("node:sqlite"); const GW = "http://127.0.0.1:8895"; const RPC = "http://192.168.76.181:8899"; const PROGRAM_ID = new PublicKey("9ALsnxXNzDBv3mokngCHbruRTWUZiWR7pf7vswS1fCpf"); const DB = "/app/data/price-history.db"; const MAX_WAIT_MS = Number(process.argv[2] || 300) * 1000; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); async function api(path, body) { const opts = body === undefined ? { method: "GET" } : { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }; const r = await fetch(GW + path, opts); let j = null; try { j = await r.json(); } catch { /* ignore */ } return { status: r.status, json: j }; } function histAt(ms) { const db = new DatabaseSync(DB, { readOnly: true }); const row = db.prepare("SELECT price FROM price_history WHERE ts <= ? ORDER BY ts DESC LIMIT 1").get(ms); const cnt = db.prepare("SELECT COUNT(*) c FROM price_history").get().c; db.close(); return { price: row ? Number(row.price) : null, count: cnt }; } async function readBet(id) { const conn = new Connection(RPC, "confirmed"); const seed = Buffer.alloc(8); seed.writeBigUInt64LE(BigInt(id)); const [pda] = PublicKey.findProgramAddressSync([Buffer.from("bet"), seed], PROGRAM_ID); const info = await conn.getAccountInfo(pda, "confirmed"); if (!info) return null; const d = info.data; return { betTime: Number(d.readBigInt64LE(16)), expireTime: Number(d.readBigInt64LE(24)), entryPrice: d.readBigUInt64LE(32).toString(), status: d.readBigUInt64LE(56).toString(), }; } async function currentPriceInt() { const r = await api("/price"); if (!r.json || r.json.price == null) return null; const [i, fr = ""] = String(r.json.price).split("."); return Number(i + fr.padEnd(8, "0").slice(0, 8)); } (async () => { console.log("=== 1. кошелёк / пополнение / ставка ==="); const w = await api("/wallet", {}); const addr = w.json.address; await api("/faucet/" + addr); const bet = await api("/bet", { side: "UP", amountUnits: "100000000", address: addr }); if (!bet.json || !bet.json.id) throw new Error("bet failed: " + JSON.stringify(bet)); const id = bet.json.id; console.log("address:", addr, "| betId:", id, "| entry:", bet.json.entryPrice); await sleep(1500); const b0 = await readBet(id); const expireMs = b0.expireTime * 1000; console.log("betTime:", new Date(b0.betTime * 1000).toISOString(), "| expireTime:", new Date(expireMs).toISOString()); const blocked = await api("/close", { id }); console.log("проба закрытия до экспирации:", blocked.status, JSON.stringify(blocked.json)); console.log("\n=== 2. ждём, пока текущая цена ОТОЙДЁТ от цены на экспирации ==="); const t0 = Date.now(); let atExpiry = null, now = null, tries = 0; while (Date.now() - t0 < MAX_WAIT_MS) { if (Date.now() < expireMs) { await sleep(1000); continue; } tries++; atExpiry = histAt(expireMs).price; now = await currentPriceInt(); process.stdout.write(` #${tries} цена@экспирации=${atExpiry} текущая=${now}\n`); if (atExpiry !== null && now !== null && atExpiry !== now) break; await sleep(2000); } if (atExpiry === null || now === null || atExpiry === now) { console.log("\nНЕ ДОЖДАЛИСЬ расхождения цен — решающий тест не выполнен (повторить позже)."); return; } console.log("\n*** РАСХОЖДЕНИЕ ЕСТЬ: цена@экспирации=" + atExpiry + ", текущая=" + now + " ***"); console.log(" уязвимый код взял бы: " + now); console.log(" исправленный должен взять: " + atExpiry); console.log("\n=== 3. ЗАКРЫТИЕ ==="); const cl = await api("/close", { id }); console.log("ответ:", cl.status, JSON.stringify(cl.json)); const exitPrice = cl.json && cl.json.exitPrice != null ? Number(cl.json.exitPrice) : null; const after = await readBet(id); console.log("\n================ ВЕРДИКТ ================"); console.log("цена на момент экспирации (история):", atExpiry); console.log("цена в момент клика (текущая) :", now); console.log("exitPrice, ушедший в транзакцию :", exitPrice); console.log(); if (exitPrice === null) { console.log("ЗАКРЫТИЕ НЕ ВЫПОЛНИЛОСЬ."); } else if (exitPrice === atExpiry && exitPrice !== now) { console.log(">>> ОКНО ЗАКРЫТО <<<"); console.log("exitPrice = цена НА МОМЕНТ ЭКСПИРАЦИИ (" + atExpiry + "), а не текущая (" + now + ")."); console.log("Момент клика больше ни на что не влияет — эксплойт закрытия по выгодной цене невозможен."); } else if (exitPrice === now && exitPrice !== atExpiry) { console.log("!!! ОКНО ОТКРЫТО !!! exitPrice = ТЕКУЩАЯ цена момента клика (" + now + "). Фикс не работает."); } else { console.log("НЕОЖИДАННЫЙ ИСХОД: exitPrice=" + exitPrice); } console.log("статус ставки после: " + after.status + (after.status === "0" ? " (open — равные цены, отдельная задача)" : "")); })().catch((e) => { console.error("ERR:", e.message); process.exit(1); });