From 749ad4c39dd611e6829f1555d3ff310b14c04af5 Mon Sep 17 00:00:00 2001 From: Davichi-1 Date: Thu, 30 Jul 2026 01:39:53 +0100 Subject: [PATCH 1/2] fix: enforce max 100 active tournament escrows per player Add a MAX_ACTIVE_ESCROWS cap (100) to prevent storage bloat attacks via create_tournament_escrow spam. Tracks per-player active escrow count in a separate Map, incrementing on creation and decrementing on release. Adds the player Address field to the TournamentEscrow struct to facilitate count management on release. Closes #864 --- contracts/game_contract/src/lib.rs | 44 ++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/contracts/game_contract/src/lib.rs b/contracts/game_contract/src/lib.rs index dcb5ef0..32d022e 100644 --- a/contracts/game_contract/src/lib.rs +++ b/contracts/game_contract/src/lib.rs @@ -140,6 +140,11 @@ const ORACLE_CONTRACT: Symbol = symbol_short!("ORACLE"); // Address of oracle co // Time-lock escrow for tournament prizes (#532) const TOURNAMENT_TIMELOCK: Symbol = symbol_short!("TL_DUR"); // u64 - lock duration in ledger sequences const TOURNAMENT_ESCROWS: Symbol = symbol_short!("TL_ESC"); // Map +const PLAYER_ACTIVE_ESCROWS: Symbol = symbol_short!("PL_ACTV"); // Map + +/// Maximum number of active (non-released) tournament escrows per player +/// to prevent storage bloat attacks. +const MAX_ACTIVE_ESCROWS: u32 = 100; // ──────────────────────────────────────────────────────────────────────────── // Multi-sig fee proposal type (#535) @@ -163,6 +168,7 @@ pub struct FeeProposal { pub struct TournamentEscrow { pub escrow_id: u64, pub game_id: u64, + pub player: Address, pub total_amount: i128, pub locked_until: u64, // ledger sequence when funds can be released pub released: bool, @@ -237,6 +243,8 @@ pub enum ContractError { EmptyBatch = 37, /// claim_puzzle_rewards_batch called with more proofs than MAX_BATCH_SIZE BatchTooLarge = 38, + /// Maximum active escrows per player exceeded (#864) + MaxActiveEscrowsExceeded = 39, } #[contract] @@ -2115,6 +2123,8 @@ impl GameContract { /// Create a time-locked escrow for a completed tournament game. /// /// Locks the total prize pool until `current_ledger + timelock_duration`. + /// Enforces a maximum of `MAX_ACTIVE_ESCROWS` per player to prevent + /// storage bloat attacks. /// Returns the escrow ID. pub fn create_tournament_escrow(env: Env, game_id: u64) -> Result { let games: Map = env @@ -2131,6 +2141,18 @@ impl GameContract { game.player1.require_auth(); + // Check active escrow cap for this player + let mut player_counts: Map = env + .storage() + .instance() + .get(&PLAYER_ACTIVE_ESCROWS) + .unwrap_or(Map::new(&env)); + + let current_count = player_counts.get(game.player1.clone()).unwrap_or(0); + if current_count >= MAX_ACTIVE_ESCROWS { + return Err(ContractError::MaxActiveEscrowsExceeded); + } + let duration: u64 = env .storage() .instance() @@ -2154,6 +2176,7 @@ impl GameContract { let escrow = TournamentEscrow { escrow_id, game_id, + player: game.player1.clone(), total_amount, locked_until, released: false, @@ -2162,6 +2185,12 @@ impl GameContract { escrows.set(escrow_id, escrow); env.storage().instance().set(&TOURNAMENT_ESCROWS, &escrows); + // Increment player's active escrow count + player_counts.set(game.player1.clone(), current_count + 1); + env.storage() + .instance() + .set(&PLAYER_ACTIVE_ESCROWS, &player_counts); + env.events().publish( (symbol_short!("tl_escrow"), symbol_short!("created")), (escrow_id, game_id, locked_until), @@ -2246,11 +2275,26 @@ impl GameContract { token_client.transfer(&contract_address, &first_winner, &remainder); } + let escrow_player = escrow.player.clone(); let mut released_escrow = escrow; released_escrow.released = true; escrows.set(escrow_id, released_escrow); env.storage().instance().set(&TOURNAMENT_ESCROWS, &escrows); + // Decrement player's active escrow count + let mut player_counts: Map = env + .storage() + .instance() + .get(&PLAYER_ACTIVE_ESCROWS) + .unwrap_or(Map::new(&env)); + let player_count = player_counts.get(escrow_player.clone()).unwrap_or(0); + if player_count > 0 { + player_counts.set(escrow_player, player_count - 1); + } + env.storage() + .instance() + .set(&PLAYER_ACTIVE_ESCROWS, &player_counts); + env.events().publish( (symbol_short!("tl_escrow"), symbol_short!("released")), escrow_id, From 74c694e2cc9d502ddf2fd5cbda1fd0a2e11c6ac8 Mon Sep 17 00:00:00 2001 From: Davichi-1 Date: Wed, 5 Aug 2026 12:37:30 +0100 Subject: [PATCH 2/2] fix: restore MaxActiveEscrowsExceeded after merge with main ContractPaused claimed error code 39 in main; re-add MaxActiveEscrowsExceeded as code 40 so create_tournament_escrow compiles again after the conflict resolution. --- contracts/game_contract/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/game_contract/src/lib.rs b/contracts/game_contract/src/lib.rs index 35ae6a0..c8c0b41 100644 --- a/contracts/game_contract/src/lib.rs +++ b/contracts/game_contract/src/lib.rs @@ -248,6 +248,8 @@ pub enum ContractError { BatchTooLarge = 38, /// Contract is paused for emergency halt (SC-11) ContractPaused = 39, + /// Player has reached the maximum number of active tournament escrows (SC-20) + MaxActiveEscrowsExceeded = 40, } #[contract]