From aadc33494dfbab8e3c83947a037b8bf7844a0781 Mon Sep 17 00:00:00 2001 From: chiliec Date: Wed, 12 Aug 2026 18:33:01 +0300 Subject: [PATCH] =?UTF-8?q?test(pm):=20replay=20test=20=E2=80=94=20settlem?= =?UTF-8?q?ent=20zero-sum=20vs=20known=20exploit=20classes=20(t16)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds tests/pm/replay/t16_settlement_exploit_classes.cpp, which LINKS the real libraries/chain/pm/parimutuel.cpp settlement math (no chain build required — see tests/pm/replay/README.md) and asserts the documented zero-sum identity Σ winner_payout + oracle_take + creator_take + lp_bonus == Σ winner.amount + losers_sum + forfeit_pool + uncovered across adversarial-but-REACHABLE inputs mapped to known DeFi/AMM exploit classes: A. oracle resolves to a non-participated outcome (empty winners) → whole pool to LP B. extreme negative forfeit_pool (heavy leverage-profit residual) → clamped to 0 and reported as `uncovered` (F1), never the uint64 wrap/mint (B3 surface) C. dust winner weight vs a large pool → rounding remainder routes to LP, no mint/loss D. max time_penalty → profit fully penalised to LP; payout == principal, never below E. distribute_lp: Σ shares == bonus for skewed principal/time mixes F. 300k-case randomized fuzz over the reachable input space The test encodes the function's real contract: every on-chain winner has weight > 0 (pm_place_bet asserts tokens_out > 0), so the total_weight==0 branch is reached only with an EMPTY winners vector — which the settle_market caller iterates in lockstep with winner_bets, so there is no out-of-bounds. All checks pass, fuzz violations = 0. Registered in tests/pm/replay/build.sh `linked` list. Built + run green with g++. --- tests/pm/replay/build.sh | 2 +- .../replay/t16_settlement_exploit_classes.cpp | 160 ++++++++++++++++++ 2 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 tests/pm/replay/t16_settlement_exploit_classes.cpp diff --git a/tests/pm/replay/build.sh b/tests/pm/replay/build.sh index c52781f2ff..88b4650140 100755 --- a/tests/pm/replay/build.sh +++ b/tests/pm/replay/build.sh @@ -22,7 +22,7 @@ out="$here/out"; mkdir -p "$out" # Cases that link the real consensus math. The rest are self-contained (native # unsigned __int128 stands in for fc::uint128_t) and need only -std=c++17. -linked="t14_f2_ledger t15_early_exit_headroom dump t2_reach t3_lp t3b_loan t3c_weight t4_cancel t5_threshold t6_cycle t10_conserve t13_penalty" +linked="t14_f2_ledger t15_early_exit_headroom t16_settlement_exploit_classes dump t2_reach t3_lp t3b_loan t3c_weight t4_cancel t5_threshold t6_cycle t10_conserve t13_penalty" build_objs() { [ -f "$out/parimutuel.o" ] && return 0 diff --git a/tests/pm/replay/t16_settlement_exploit_classes.cpp b/tests/pm/replay/t16_settlement_exploit_classes.cpp new file mode 100644 index 0000000000..a6f65d6858 --- /dev/null +++ b/tests/pm/replay/t16_settlement_exploit_classes.cpp @@ -0,0 +1,160 @@ +// t16_settlement_exploit_classes.cpp — replay the REAL settlement math (parimutuel.cpp, +// linked, not modelled) against adversarial-but-REACHABLE inputs drawn from known +// DeFi/AMM exploit classes, and assert the documented zero-sum identity holds every time: +// +// Σ winner_payout + oracle_take + creator_take + lp_bonus +// == Σ winner.amount + losers_sum + forfeit_pool + uncovered +// +// (LP principal is returned 1:1 by the caller and cancels; `uncovered` sits on the input +// side as the F1 shortfall the caller charges to LP principal — so the identity holds +// UNCONDITIONALLY and nothing is ever minted.) +// +// IMPORTANT — model the function's real contract: +// * Every on-chain winner has weight > 0 (pm_place_bet asserts tokens_out > 0). The +// total_weight==0 branch is ONLY reached with an EMPTY winners vector (no bet on the +// winning outcome), and settle_market's caller iterates winner_bets in lockstep, so an +// empty winners vector ⇒ empty winner_payout ⇒ no out-of-bounds. We therefore feed +// weight>0 for every winner, and test the no-winner case with an EMPTY vector. +// * Fees are bounded on-chain (oracle ≤ pm_max_oracle_fee_percent, and settle_market +// clamps the early-exit bucket so fees+bucket ≤ losers_sum). We keep fee params in +// realistic ranges; the negative-forfeit path is where `uncovered` legitimately fires. +// +// Exploit classes probed: +// A. Oracle resolves to a NON-participated outcome (empty winners) — whole pool → lp_bonus. +// B. Extreme negative forfeit_pool (heavy leverage-profit residual) — must clamp to 0 and +// report `uncovered`, never wrap/mint (the B3/F1 surface). +// C. Dust winner weight vs a large pool — rounding routes remainder to LP, never mints. +// D. Max time_penalty — profit fully penalised to LP; payout == principal, never below. +// E. distribute_lp conservation: Σ shares == bonus for adversarial principal/time mixes. +// F. Randomized fuzz of the zero-sum identity across the full reachable input space. +// +// Build: part of tests/pm/replay (links libraries/chain/pm/parimutuel.cpp). See build.sh. +#include +#include +#include +#include +using namespace graphene::chain::pm; + +static int failures = 0; + +// Assert the zero-sum identity + payout sanity for one settlement. Returns delta (out - in). +static long long check(const char* tag, const settle_params& p, + const std::vector& w, bool verbose) { + settle_result r = compute_settlement(p, w); + + long long out = (long long)r.oracle_take + r.creator_take + r.lp_bonus; + for (size_t i = 0; i < r.winner_payout.size(); ++i) out += r.winner_payout[i]; + + long long in = (long long)p.losers_sum + p.forfeit_pool + r.uncovered; + for (const auto& x : w) in += x.amount; + + long long delta = out - in; + bool ok = (delta == 0); + + // winner_payout is either empty (no winners) or parallel to w; each winner is paid at + // least their principal (never a silent stake haircut), and nothing negative escapes. + bool payout_ok = (r.winner_payout.size() == w.size()) || (w.empty() && r.winner_payout.empty()); + for (size_t i = 0; i < r.winner_payout.size() && i < w.size(); ++i) + if (r.winner_payout[i] < w[i].amount) payout_ok = false; + if (r.oracle_take < 0 || r.creator_take < 0 || r.lp_bonus < 0 || r.uncovered < 0) payout_ok = false; + + if (!ok || !payout_ok) ++failures; + if (verbose) + printf(" %-34s Sin=%-12lld Sout=%-12lld unc=%-12lld %s%s\n", + tag, in, out, (long long)r.uncovered, + ok ? "conserved" : "*** MINT/LOSS ***", + payout_ok ? "" : " *** BAD PAYOUT ***"); + return delta; +} + +int main() { + printf("=== A. oracle resolves to a NON-participated outcome (empty winners) ===\n"); + { + settle_params p; p.losers_sum = 100000; p.forfeit_pool = 5000; + p.oracle_fee_percent = 100; p.creator_fee_percent = 100; p.liquidity_fee_percent = 200; + check("no winners, +forfeit", p, {}, true); + settle_params p2 = p; p2.forfeit_pool = -50000; + check("no winners, negative forfeit", p2, {}, true); + settle_params p3 = p; p3.losers_sum = 0; p3.forfeit_pool = 0; + check("no winners, empty pot", p3, {}, true); + } + + printf("\n=== B. extreme negative forfeit_pool (leverage-profit residual) → uncovered, no wrap ===\n"); + { + settle_params p; p.losers_sum = 50000; + p.oracle_fee_percent = 100; p.creator_fee_percent = 100; p.liquidity_fee_percent = 200; + for (int64_t fp : { -1LL, -49999LL, -50000LL, -1000000LL, -9000000000000000LL }) + { p.forfeit_pool = fp; check("neg forfeit sweep", p, {{1000,1000,0},{3000,3000,0}}, true); } + } + + printf("\n=== C. dust winner weight vs large pool (rounding routes to LP) ===\n"); + { + settle_params p; p.losers_sum = 1000000; p.forfeit_pool = 0; + check("weight=1 vs 1M pool", p, {{0, 1, 0}}, true); + check("weight=1 and weight=1e9", p, {{0, 1, 0}, {0, 1000000000, 0}}, true); + check("uneven weights, +stake", p, {{5000, 7, 0}, {5000, 999983, 0}}, true); + } + + printf("\n=== D. max time_penalty — profit → LP, payout == principal, never below ===\n"); + { + settle_params p; p.losers_sum = 1000000; p.forfeit_pool = 0; + p.oracle_fee_percent = 100; p.creator_fee_percent = 100; p.liquidity_fee_percent = 200; + check("full time_penalty", p, {{5000, 5000, 1000000}}, true); + check("mixed penalty", p, {{5000, 5000, 500000}, {5000, 5000, 0}}, true); + } + + printf("\n=== E. distribute_lp conservation (Σ shares == bonus) ===\n"); + { + struct LPCase { const char* tag; std::vector lps; int64_t bonus; }; + std::vector cases = { + {"equal principal/time", {{1000, 3600}, {1000, 3600}, {1000, 3600}}, 100003}, + {"skewed principal", {{1, 60}, {1000000, 60}}, 777}, + {"zero-time simultaneous", {{500, 0}, {500, 0}}, 1001}, + {"one huge one tiny time", {{1000, 31536000}, {1000, 1}}, 99999}, + {"single LP", {{1234, 500}}, 4242}, + {"zero bonus", {{1000, 60}, {2000, 120}}, 0}, + }; + for (auto& c : cases) { + auto shares = distribute_lp(c.lps, c.bonus); + int64_t sum = 0; bool neg = false; + for (auto s : shares) { sum += s; if (s < 0) neg = true; } + bool ok = (sum == c.bonus) && !neg; + if (!ok) ++failures; + printf(" %-26s bonus=%-8lld Σshares=%-8lld %s\n", + c.tag, (long long)c.bonus, (long long)sum, + ok ? "conserved" : "*** LEAK ***"); + } + } + + printf("\n=== F. randomized adversarial sweep within the REACHABLE input space ===\n"); + { + uint64_t seed = 0x9E3779B97F4A7C15ULL; + auto rnd = [&]() { seed ^= seed << 13; seed ^= seed >> 7; seed ^= seed << 17; return seed; }; + long long checked = 0, bad = 0; + for (int i = 0; i < 300000; ++i) { + settle_params p; + p.losers_sum = (int64_t)(rnd() % 2000000); + p.forfeit_pool = (int64_t)(rnd() % 4000000) - 2000000; // signed: leverage residual + p.oracle_fixed_fee = (int64_t)(rnd() % 200000); + // fees kept in realistic on-chain ranges (each ≤ 5%); their sum ≤ ~15% << 100%. + p.oracle_fee_percent = (uint16_t)(rnd() % 500); + p.creator_fee_percent = (uint16_t)(rnd() % 500); + p.liquidity_fee_percent = (uint16_t)(rnd() % 500); + std::vector w; + int nw = (int)(rnd() % 4); // 0..3 winners; 0 ⇒ empty (valid) + for (int k = 0; k < nw; ++k) + w.push_back({ (int64_t)(rnd() % 100000), + (int64_t)(rnd() % 1000000) + 1, // weight ALWAYS > 0 (on-chain invariant) + (uint32_t)(rnd() % 1000001) }); // penalty ∈ [0, 1e6] + if (check("", p, w, false) != 0) ++bad; + ++checked; + } + printf(" fuzzed %lld settlements (reachable inputs); zero-sum violations = %lld\n", checked, bad); + } + + printf("\n%s (%d failing assertions)\n", + failures == 0 ? "ALL EXPLOIT-CLASS CHECKS PASS — settlement is zero-sum on every reachable input" + : "*** FAILURES ***", + failures); + return failures == 0 ? 0 : 1; +}