docs(sec): SEC-16 — безвозвратная утечка ренты PDA-ставки с кошелька игрока

Найдено при приёмке боевого домена smart-updown.binom.pw (симптом «SOL не обновляется»).

- SEC-16: при create_bet ренту PDA-аккаунта ставки (1 559 040 lamports = (96+32+128)*6960)
  платит БЕТТОР, а не шлюз; баланс шлюза-админа при этом не меняется (замер whose-fee.cjs).
  Аккаунт не закрывается никогда (в контракте 9ALs четыре инструкции, ни одной close = bettor)
  → рента теряется безвозвратно. В цепи 75 аккаунтов = 0.116928 SOL заперто, растёт линейно.
  Вердикт: на тестах терпимо (маскируется краном, 5 SOL = ~3207 раундов), перед mainnet — блокер.
- TASKS.md: фича «если цена не изменилась — подождать ещё 15 с» (НЕ доделана), заметка про
  боевой домен на общем TS-шлюзе и лаг крана 8–13 с.
- tools: доказательные скрипты (whose-fee, bet-pda-rent, faucet-latency, sol-balance-cycle,
  settle-math-test, backend-full-test) + README с питфоллами замера.
- k8s/smart-updown-api-ingress.yaml: /api,/ws вынесены вне helm (релиз игры их сносил).
This commit is contained in:
Hermes Agent
2026-09-15 05:57:34 +03:00
parent b8a1f8dc39
commit 101a4b60a9
10 changed files with 603 additions and 4 deletions
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env node
// Проверка: обновляется ли SOL после полного цикла ставки (на живом домене).
const B = "https://smart-updown.binom.pw/api";
const RPC = "https://smart-updown.binom.pw/solana-rpc/";
const WS_URL = "wss://smart-updown.binom.pw/api/ws";
const WebSocket = require(process.env.WS_MODULE || "ws");
const j = async (p, init) => {
const r = await fetch(B + p, init);
const t = await r.text();
try { return { s: r.status, d: JSON.parse(t) }; } catch { return { s: r.status, d: t }; }
};
const rpcBal = async (addr) => {
const r = await fetch(RPC, {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "getBalance", params: [addr] }),
});
const d = await r.json();
return d.result.value;
};
(async () => {
const log = (k, v) => console.log(`${k.padEnd(34)} ${v}`);
// 1. свежий кошелёк
const w = await j("/wallet", { method: "POST" });
const addr = w.d.address;
log("1. /wallet → адрес", addr);
let bal = await rpcBal(addr);
log("2. RPC getBalance (пусто)", bal);
// 2. кран
const f = await j(`/faucet/${addr}`);
log("3. /faucet", f.s + " sol=" + f.d.sol);
bal = await rpcBal(addr);
log("4. RPC после крана", bal + " (" + (bal / 1e9).toFixed(4) + " SOL)");
const gw1 = await j(`/balance/${addr}`);
log("5. шлюз /balance", JSON.stringify(gw1.d));
// 3. WS
const ws = new WebSocket(WS_URL);
const events = [];
await new Promise((res) => { ws.on("open", res); ws.on("error", res); setTimeout(res, 8000); });
ws.on("message", (m) => { try { const e = JSON.parse(m); if (e.type && e.type !== "price") events.push(e); } catch {} });
log("6. WS", ws.readyState === 1 ? "подключён" : "НЕ подключён");
// 4. ставка
const bet = await j("/bet", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ address: addr, side: "UP", amountUnits: "100000000" }),
});
log("7. POST /bet", bet.s + " " + JSON.stringify(bet.d).slice(0, 110));
const before = await rpcBal(addr);
log("8. RPC сразу после ставки", before + " (" + (before / 1e9).toFixed(6) + " SOL)");
// 5. ждём авто-закрытие
const id = bet.d.id;
await new Promise((r) => setTimeout(r, 20000));
const after = await rpcBal(addr);
log("9. RPC после закрытия (~20с)", after + " (" + (after / 1e9).toFixed(6) + " SOL)");
const gw2 = await j(`/balance/${addr}`);
log("10. шлюз /balance после", JSON.stringify(gw2.d));
log("11. WS-события", JSON.stringify(events).slice(0, 300));
const delta = after - before;
log("12. ДЕЛЬТА на цепи", delta + " lamports (" + (delta / 1e9).toFixed(6) + " SOL)");
log("13. читается ли /api/bet/" + id, (await j(`/bet/${id}`)).s);
ws.close();
process.exit(0);
})();