Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion tests/pm/replay/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 t17_leverage_math_invariants 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 t17_leverage_math_invariants 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
Expand Down
160 changes: 160 additions & 0 deletions tests/pm/replay/t16_settlement_exploit_classes.cpp
Original file line number Diff line number Diff line change
@@ -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 <graphene/chain/pm/parimutuel.hpp>
#include <cstdio>
#include <vector>
#include <cstdint>
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<winner_in>& 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<lp_in> lps; int64_t bonus; };
std::vector<LPCase> 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<winner_in> 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;
}