Files
pump-game-ops/tools/bet-pda-rent.cjs
T
Hermes Agent 101a4b60a9 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 (релиз игры их сносил).
2026-09-15 05:57:34 +03:00

38 lines
2.1 KiB
JavaScript

// Сколько lamports лежит в PDA ставок и кому принадлежит рента
const { PublicKey } = require(process.env.SOLANA_WEB3_MODULE || "@solana/web3.js");
const RPC = "https://smart-updown.binom.pw/solana-rpc/";
const PROGRAM = new PublicKey("9ALsnxXNzDBv3mokngCHbruRTWUZiWR7pf7vswS1fCpf");
const BET_SEED = Buffer.from("bet");
const GLOBAL_SEED = Buffer.from("global");
const VAULT_SEED = Buffer.from("vault");
const rpc = async (m, p) => {
const r = await fetch(RPC, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: m, params: p }) });
const x = await r.json();
if (x.error) throw new Error(JSON.stringify(x.error).slice(0, 150));
return x.result;
};
const betPda = (id) => { const b = Buffer.alloc(8); b.writeBigUInt64LE(BigInt(id)); return PublicKey.findProgramAddressSync([BET_SEED, b], PROGRAM)[0]; };
(async () => {
console.log(`программа: ${PROGRAM.toBase58()}`);
const g = PublicKey.findProgramAddressSync([GLOBAL_SEED], PROGRAM)[0];
const v = PublicKey.findProgramAddressSync([GLOBAL_SEED, VAULT_SEED], PROGRAM)[0];
console.log(`global PDA: ${g.toBase58()}`);
console.log(`vault PDA: ${v.toBase58()}\n`);
let total = 0n, alive = 0;
for (const id of [60, 65, 67, 68, 69, 70, 71, 72, 73, 74]) {
const p = betPda(id);
const r = await rpc("getAccountInfo", [p.toBase58(), { encoding: "base64" }]);
const info = r.value;
if (!info) { console.log(`ставка ${id}: PDA ${p.toBase58().slice(0, 8)}… НЕТ в цепи`); continue; }
alive++; total += BigInt(info.lamports);
console.log(`ставка ${id}: PDA ${p.toBase58().slice(0, 8)}… lamports=${info.lamports} owner=${info.owner.slice(0, 8)}… размер=${info.data[0].length * 3 / 4} байт`);
}
console.log(`\nвсего PDA ставок: ${alive}; суммарно lamports в них: ${total} (${Number(total) / 1e9} SOL)`);
const gr = await rpc("getAccountInfo", [g.toBase58(), { encoding: "base64" }]);
console.log(`global PDA lamports: ${gr.value ? gr.value.lamports : "нет"}`);
process.exit(0);
})();