diff --git a/contracts/predictify-hybrid/src/bets.rs b/contracts/predictify-hybrid/src/bets.rs index 9a14364a..b71d62b0 100644 --- a/contracts/predictify-hybrid/src/bets.rs +++ b/contracts/predictify-hybrid/src/bets.rs @@ -310,6 +310,10 @@ impl BetManager { // Require authentication from the user user.require_auth(); + // Enforce global per-ledger bet cap + let rate_limiter = crate::rate_limiter::RateLimiter::new(env.clone()); + rate_limiter.rate_limit_global_bets_per_ledger()?; + // Slippage check: verify live fee is not above the maximum acceptable threshold // max_fee_bps == 0 means no slippage guard if max_fee_bps > 0 { @@ -453,6 +457,10 @@ impl BetManager { for bet_data in bets.iter() { let (market_id, outcome, amount) = bet_data; + // Enforce global per-ledger bet cap for each bet in the batch + let rate_limiter = crate::rate_limiter::RateLimiter::new(env.clone()); + rate_limiter.rate_limit_global_bets_per_ledger()?; + // Get and validate market let market = MarketStateManager::get_market(env, &market_id)?; BetValidator::validate_market_for_betting(env, &market)?; diff --git a/contracts/predictify-hybrid/src/err.rs b/contracts/predictify-hybrid/src/err.rs index 29996cd1..d28772be 100644 --- a/contracts/predictify-hybrid/src/err.rs +++ b/contracts/predictify-hybrid/src/err.rs @@ -245,6 +245,8 @@ pub enum Error { ReplayedOverride = 526, /// Oracle quote is an outlier relative to the rolling median history. OracleQuoteOutlier = 527, + /// Global per-ledger bet cap has been exceeded to dampen flash-trading bursts. + PerLedgerBetCapExceeded = 528, } // ===== ERROR CATEGORIZATION AND RECOVERY SYSTEM ===== diff --git a/contracts/predictify-hybrid/src/rate_limiter.rs b/contracts/predictify-hybrid/src/rate_limiter.rs index 03618776..16a3dbdf 100644 --- a/contracts/predictify-hybrid/src/rate_limiter.rs +++ b/contracts/predictify-hybrid/src/rate_limiter.rs @@ -173,6 +173,42 @@ impl RateLimiter { Ok(()) } + /// Rate limit global bets per ledger to dampen flash-trading bursts. + /// Resets implicitly when ledger sequence advances. + pub fn rate_limit_global_bets_per_ledger(&self) -> Result<(), crate::err::Error> { + let cap: u32 = self.env.storage().persistent().get(&crate::storage::DataKey::PerLedgerBetCap).unwrap_or(0); + if cap == 0 { + return Ok(()); + } + + let seq = self.env.ledger().sequence(); + let counter_key = crate::storage::DataKey::PerLedgerBetCounter; + + // Stored as (ledger_sequence, count) + let mut count: (u32, u32) = self.env.storage().temporary().get(&counter_key).unwrap_or((seq, 0)); + + if count.0 != seq { + count = (seq, 0); + } + + if count.1 >= cap { + return Err(crate::err::Error::PerLedgerBetCapExceeded); + } + + count.1 += 1; + self.env.storage().temporary().set(&counter_key, &count); + self.env.storage().temporary().extend_ttl(&counter_key, 100, 100); + + Ok(()) + } + + /// Set the global per-ledger bet cap (admin only). + pub fn set_per_ledger_bet_cap(&self, admin: Address, cap: u32) -> Result<(), RateLimiterError> { + admin.require_auth(); + self.env.storage().persistent().set(&crate::storage::DataKey::PerLedgerBetCap, &cap); + Ok(()) + } + /// Rate limit event creation: max events per admin per time window. /// Caller (e.g. create_market) must have already authenticated admin. pub fn rate_limit_admin_events(&self, admin: Address) -> Result<(), RateLimiterError> { diff --git a/contracts/predictify-hybrid/src/storage.rs b/contracts/predictify-hybrid/src/storage.rs index 589cb3c1..da670a4c 100644 --- a/contracts/predictify-hybrid/src/storage.rs +++ b/contracts/predictify-hybrid/src/storage.rs @@ -87,6 +87,10 @@ pub enum DataKey { MarketCache(Symbol), /// Nonce for admin override replay protection. AdminOverrideNonce(Address), + /// Global counter for bets placed in the current ledger. + PerLedgerBetCounter, + /// Configurable per-ledger bet cap. + PerLedgerBetCap, } /// Storage format version for migration tracking diff --git a/scripts/check_wasm_size.sh b/scripts/check_wasm_size.sh index 65e18946..2a1df216 100755 --- a/scripts/check_wasm_size.sh +++ b/scripts/check_wasm_size.sh @@ -1,6 +1,8 @@ #!/bin/bash set -e + + # Default budget: 768 KiB = 768 * 1024 = 786432 bytes BUDGET=${WASM_SIZE_BUDGET:-786432}