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).

Остаток: в контракте верхней границы окна по-прежнему нет (второй слой, не критично);
серверное авто-закрытие — следующая итерация.
This commit is contained in:
2026-09-13 14:47:21 +03:00
parent 4caddd6a73
commit 6e7132e935
5 changed files with 466 additions and 5 deletions
+104
View File
@@ -0,0 +1,104 @@
/**
* Проверка схемы «цена закрытия из истории» на РЕАЛЬНОЙ ставке.
* Читает аккаунт Bet из localnet СЫРЫМИ БАЙТАМИ (без Anchor-обёрток),
* берёт bet_time/expire_time/entry_price и спрашивает Postgres binance-market.
*
* Раскладка Bet (Anchor, после 8-байтового дискриминатора):
* offset 8 : amount u64
* offset 16 : bet_time i64
* offset 24 : expire_time i64
* offset 32 : entry_price u64
* offset 40 : exit_price u64
* offset 48 : side u64
* offset 56 : status u64
* offset 64 : bettor Pubkey(32)
*/
const { Connection, PublicKey } = require("@solana/web3.js");
const { Client } = require("pg");
const PROGRAM_ID = new PublicKey(process.argv[2] || "9ALsnxXNzDBv3mokngCHbruRTWUZiWR7pf7vswS1fCpf");
const BET_ID = Number(process.argv[3] || 6);
const RPC = "http://192.168.76.181:8899";
const u64 = (b, o) => b.readBigUInt64LE(o);
const i64 = (b, o) => b.readBigInt64LE(o);
(async () => {
const conn = new Connection(RPC, "confirmed");
const seeds = Buffer.concat([Buffer.from("bet"), (() => {
const a = Buffer.alloc(8); a.writeBigUInt64LE(BigInt(BET_ID)); return a;
})()]);
const [betPda] = PublicKey.findProgramAddressSync([seeds], PROGRAM_ID);
const info = await conn.getAccountInfo(betPda, "confirmed");
if (!info) throw new Error("bet account not found: " + betPda.toBase58());
const d = info.data;
const amount = u64(d, 8);
const betTime = Number(i64(d, 16));
const exp = Number(i64(d, 24));
const entry = u64(d, 32);
const exitP = u64(d, 40);
const side = u64(d, 48);
const status = u64(d, 56);
const bettor = new PublicKey(d.subarray(64, 96)).toBase58();
console.log("=== СТАВКА №" + BET_ID + " (сырые байты аккаунта) ===");
console.log("PDA :", betPda.toBase58());
console.log("amount :", amount.toString());
console.log("bet_time :", betTime, new Date(betTime * 1000).toISOString());
console.log("expire_time:", exp, new Date(exp * 1000).toISOString(), "(=bet_time+" + (exp - betTime) + "с)");
console.log("entry_price:", entry.toString(), " exit_price:", exitP.toString());
console.log("side:", side.toString(), " status:", status.toString(), " bettor:", bettor);
console.log("СЕЙЧАС :", Math.floor(Date.now() / 1000), new Date().toISOString());
console.log("просрочена на:", Math.floor(Date.now() / 1000) - exp, "с");
const c = new Client({ host: "192.168.76.182", port: 5434, user: "postgres", password: "postgres", database: "binance_market" });
await c.connect();
const q = async (label, sql, params) => {
const r = await c.query(sql, params);
console.log("\n--- " + label + " ---");
if (!r.rows.length) console.log("(ПУСТО — цены нет вовсе)");
for (const row of r.rows) console.log(JSON.stringify(row));
return r.rows;
};
const bMs = betTime * 1000, eMs = exp * 1000;
await q("1) сделки в окне ставки [bet_time, expire]", `
SELECT price::text AS price, event_time, to_timestamp(event_time/1000) AS t
FROM trade_event WHERE symbol='solusdt' AND event_time BETWEEN $1 AND $2
ORDER BY event_time`, [bMs, eMs]);
const e1 = await q("2) ENTRY = последняя цена НЕ ПОЗЖЕ bet_time", `
SELECT price::text AS price, event_time, to_timestamp(event_time/1000) AS t
FROM trade_event WHERE symbol='solusdt' AND event_time <= $1
ORDER BY event_time DESC LIMIT 1`, [bMs]);
const e2 = await q("3) EXIT = последняя цена НЕ ПОЗЖЕ expire_time", `
SELECT price::text AS price, event_time, to_timestamp(event_time/1000) AS t
FROM trade_event WHERE symbol='solusdt' AND event_time <= $1
ORDER BY event_time DESC LIMIT 1`, [eMs]);
if (e1[0] && e2[0]) {
const p1 = BigInt(e1[0].price.replace(".", "").padEnd(0));
const a = e1[0].price, b = e2[0].price;
console.log("\n=== ИТОГ ПО СХЕМЕ ===");
console.log("entry из истории:", a, "| entry on-chain:", entry.toString(), "(on-chain = price*1e8)");
console.log("exit из истории:", b);
const toI = (s) => { const [i, f = ""] = s.split("."); return BigInt(i + f.padEnd(8, "0").slice(0, 8)); };
const ai = toI(a), bi = toI(b);
console.log("entry(int):", ai.toString(), " exit(int):", bi.toString());
console.log("вердикт по контракту:", bi > ai ? "UP выиграл" : bi < ai ? "DOWN выиграл" : "РАВНО — ставка зависает (текущий контракт не финализирует)");
}
await q("4) ЧТО БЫЛО БЫ при 'ждать выгодной цены' (после экспирации, +2 мин)", `
SELECT min(price)::text AS min_p, max(price)::text AS max_p, count(*) AS n,
min(to_timestamp(event_time/1000)) AS с, max(to_timestamp(event_time/1000)) AS по
FROM trade_event WHERE symbol='solusdt' AND event_time > $1 AND event_time <= $2`,
[eMs, (exp + 120) * 1000]);
await c.end();
})().catch((e) => { console.error("ERR:", e.message); process.exit(1); });