Files
pump-game-ops/tools/exploit-decisive.cjs
subochev 6e7132e935 docs(security): SEC-1, SEC-2, SEC-4 закрыты по сути — цена закрытия из истории
Механизм: шлюз берёт цену закрытия из локальной истории на момент expire_time
ставки (priceHistory, node:sqlite, ретеншен 24ч), а не текущую. Момент клика
перестаёт влиять на исход — окно выбора времени закрыто.

Живая проверка (ставка №10, релиз updown-relayer:3):
  цена на момент экспирации 9977000000
  текущая цена в момент клика 9976000000
  exitPrice в транзакции 9977000000  -> взята история, не 'сейчас'
  статус payout_done

Также: tools/ — скрипты проверки (price-history-probe, exploit-decisive,
exploit-check, check-close-window.sh).

Остаток: в контракте верхней границы окна по-прежнему нет (второй слой, не критично);
серверное авто-закрытие — следующая итерация.
2026-09-13 14:47:21 +03:00

129 lines
6.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* РЕШАЮЩАЯ проверка: цена закрытия берётся из истории на момент экспирации,
* а НЕ из текущей цены в момент клика.
*
* Почему прошлые прогоны были слабыми: сравнивали 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); });