Files
pump-game-ops/tools/exploit-check.cjs
T
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

145 lines
7.4 KiB
JavaScript
Raw 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.
/**
* ЖИВАЯ проверка: закрыто ли окно выбора времени закрытия.
* Запускается ВНУТРИ пода шлюза (обращается к 127.0.0.1:8895 и читает ставку из RPC).
*
* Логика доказательства:
* - ставим ставку, запоминаем betTime/expireTime (сырые байты аккаунта Bet)
* - ЖДЁМ существенно дольше экспирации
* - считаем: price_at_expiry (история, ts <= expireTime) и price_now (текущая)
* - закрываем ставку и смотрим, какая цена ушла в транзакцию
*
* ОКНО ЗАКРЫТО, если exitPrice == price_at_expiry и при этом price_now != price_at_expiry
* (т.е. цена взята из прошлого, а не с момента клика).
*/
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 WAIT_MS = Number(process.argv[2] || 75) * 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 priceAtExpiry(expireMs) {
const db = new DatabaseSync(DB);
const row = db.prepare("SELECT price FROM price_history WHERE ts <= ? ORDER BY ts DESC LIMIT 1").get(expireMs);
const cnt = db.prepare("SELECT COUNT(*) c FROM price_history").get().c;
const first = db.prepare("SELECT ts FROM price_history ORDER BY ts ASC LIMIT 1").get();
db.close();
return { price: row ? row.price : null, count: cnt, firstTs: first ? first.ts : null };
}
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 {
pda: pda.toBase58(),
betTime: Number(d.readBigInt64LE(16)),
expireTime: Number(d.readBigInt64LE(24)),
entryPrice: d.readBigUInt64LE(32).toString(),
exitPrice: d.readBigUInt64LE(40).toString(),
status: d.readBigUInt64LE(56).toString(),
};
}
(async () => {
console.log("=== 1. кошелёк ===");
const w = await api("/wallet", {});
if (!w.json || !w.json.address) throw new Error("wallet failed: " + JSON.stringify(w));
const addr = w.json.address;
console.log("address:", addr);
console.log("=== 2. пополнение ===");
const f = await api("/faucet/" + addr);
console.log("faucet:", f.status, JSON.stringify(f.json).slice(0, 140));
console.log("=== 3. ставка ===");
const bet = await api("/bet", { side: "UP", amountUnits: "100000000", address: addr });
console.log("bet:", bet.status, JSON.stringify(bet.json).slice(0, 200));
if (!bet.json || !bet.json.id) throw new Error("bet failed");
const id = bet.json.id;
const entryResp = String(bet.json.entryPrice);
await sleep(1500);
const b0 = await readBet(id);
console.log("\n--- ставка on-chain ---");
console.log("betTime :", b0.betTime, new Date(b0.betTime * 1000).toISOString());
console.log("expireTime:", b0.expireTime, new Date(b0.expireTime * 1000).toISOString(),
"(+" + (b0.expireTime - b0.betTime) + "с)");
console.log("entry(on-chain):", b0.entryPrice, "| entry(/bet ответ):", entryResp);
console.log("\n=== 4. ПРОБА: закрыть ДО экспирации (должен быть 400) ===");
const early = await api("/close", { id });
const stillOpen = await readBet(id);
if (stillOpen.status === "0") {
console.log("ответ:", early.status, JSON.stringify(early.json));
console.log(early.status === 400 ? " -> ок, отказ до экспирации" : " -> ВНИМАНИЕ: ожидался 400");
} else {
console.log(" (ставка уже закрылась сама/успела — пропускаем пробу)");
}
console.log("\n=== 5. ЖДЁМ " + (WAIT_MS / 1000) + "с (экспирация давно прошла) ===");
await sleep(WAIT_MS);
const b1 = await readBet(id);
const expireMs = b1.expireTime * 1000;
const hist = priceAtExpiry(expireMs);
const nowResp = await api("/price");
const priceNowStr = nowResp.json ? String(nowResp.json.price) : null;
const priceNowInt = priceNowStr ? Number(priceNowStr.replace(".", "").padEnd(0) === "" ? "0" : priceNowStr.replace(".", "")) : null;
const toInt = (s) => { const [i, fr = ""] = String(s).split("."); return Number(i + fr.padEnd(8, "0").slice(0, 8)); };
const nowInt = priceNowStr ? toInt(priceNowStr) : null;
console.log("цена на момент экспирации (из истории):", hist.price, "(строк в истории:", hist.count + ")");
console.log("текущая цена (момент клика) :", nowInt, "(" + priceNowStr + ")");
console.log("entry :", b1.entryPrice);
console.log("\n=== 6. ЗАКРЫТИЕ ===");
const cl = await api("/close", { id });
console.log("ответ:", cl.status, JSON.stringify(cl.json));
const exitPrice = cl.json && cl.json.exitPrice != null ? String(cl.json.exitPrice) : null;
const b2 = await readBet(id);
console.log("после закрытия: status=" + b2.status, "exitPrice=" + b2.exitPrice);
console.log("\n================ ВЕРДИКТ ================");
console.log("price_at_expiry (история) :", hist.price);
console.log("exitPrice (ушло в tx) :", exitPrice);
console.log("price_now (момент клика) :", nowInt);
console.log();
if (exitPrice === null) {
console.log("ЗАКРЫТИЕ НЕ ВЫПОЛНИЛОСЬ — см. ответ выше.");
} else if (hist.price === null) {
console.log("НЕТ ДАННЫХ В ИСТОРИИ — проверить запись.");
} else if (String(hist.price) === String(exitPrice)) {
if (String(nowInt) !== String(hist.price)) {
console.log("*** ОКНО ЗАКРЫТО ***");
console.log("exitPrice взят ИЗ ИСТОРИИ на момент экспирации и НЕ равен цене момента клика.");
console.log("Игрок физически не может выбрать выгодный момент: цена уже зафиксирована прошлым.");
} else {
console.log("СОВПАЛО (но цена не двигалась) — тест слабый, цена сейчас та же. Повторить позже.");
}
} else {
console.log("!!! ОКНО ОТКРЫТО !!! exitPrice (" + exitPrice + ") != цена на момент экспирации (" + hist.price + ")");
}
if (b2.status === "0") console.log("\n(ставка осталась open — exit==entry: равные цены, отдельная задача)");
})().catch((e) => { console.error("ERR:", e.message); process.exit(1); });