@@ -23,6 +23,53 @@ use crate::events::PerformanceMetricEvent;
2323
2424use crate :: err:: Error ;
2525
26+ // ===== DEFAULT GAS REGRESSION LIMITS =====
27+ //
28+ // These constants define the hard upper bounds for gas consumption on the
29+ // two highest-traffic critical paths: market creation and winnings claim.
30+ //
31+ // Rationale:
32+ // - Derived from mock-delta measurements in performance_benchmarks.rs
33+ // with generous headroom to avoid false positives on normal variation.
34+ // - If an operation's gas usage exceeds the regression limit, the
35+ // transaction panics with GasBudgetExceeded, preventing regressions
36+ // from shipping unnoticed.
37+ // - Admins may override these defaults via set_limit() at runtime.
38+ // - These limits are intentionally conservative; tighten once real
39+ // `stellar contract invoke --cost` p99 values are available.
40+ //
41+ // Trade-offs:
42+ // - Too tight → false positives on normal input variation (esp. long
43+ // question strings, many outcomes, many voters)
44+ // - Too loose → regressions slip through silently
45+ // - Current values use 2x the mock-delta p95 as a safety margin.
46+
47+ /// Default maximum gas (CPU instructions) allowed for `create_market`.
48+ /// Covers admin auth, input validation, oracle config validation,
49+ /// ID generation, market struct construction, and persistent storage writes.
50+ pub const DEFAULT_CREATE_MARKET_GAS_LIMIT : u64 = 5_000_000 ;
51+
52+ /// Default maximum gas (CPU instructions) allowed for `claim_winnings`.
53+ /// Covers auth, market read, resolution cache lookup, payout
54+ /// arithmetic, balance credit, and claimed-flag write.
55+ pub const DEFAULT_CLAIM_WINNINGS_GAS_LIMIT : u64 = 2_000_000 ;
56+
57+ /// Retrieves the default regression limit for an operation.
58+ ///
59+ /// Returns `None` for operations that don't have a hardcoded default.
60+ /// These defaults act as fallbacks when no admin-configured limit exists
61+ /// via `set_limit()`.
62+ fn get_default_limit ( operation : & Symbol ) -> Option < u64 > {
63+ // Use to_string comparison because Soroban Symbol doesn't implement
64+ // direct equality with &str in a const-compatible way.
65+ let op_str = alloc:: format!( "{}" , operation) ;
66+ match op_str. as_str ( ) {
67+ "create" => Some ( DEFAULT_CREATE_MARKET_GAS_LIMIT ) ,
68+ "claim" => Some ( DEFAULT_CLAIM_WINNINGS_GAS_LIMIT ) ,
69+ _ => None ,
70+ }
71+ }
72+
2673/// Stores the gas limit configured by an admin for a specific operation.
2774#[ contracttype]
2875#[ derive( Clone , Debug , Eq , PartialEq ) ]
@@ -154,6 +201,40 @@ impl GasTracker {
154201 . set ( & GasConfigKey :: MemLimit ( operation) , & max_mem) ;
155202 }
156203
204+ /// Seeds default regression limits for `create_market` and `claim_winnings`
205+ /// into instance storage.
206+ ///
207+ /// This should be called once during contract initialization so that
208+ /// `end_tracking` and `record_with_alert` have baseline limits even
209+ /// before an admin explicitly calls `set_limit`.
210+ ///
211+ /// If an admin later calls `set_limit` for the same operation, the
212+ /// admin value takes precedence (checked first in `end_tracking`).
213+ pub fn set_default_limits ( env : & Env ) {
214+ env. storage ( ) . instance ( ) . set (
215+ & GasConfigKey :: GasLimit ( symbol_short ! ( "create" ) ) ,
216+ & DEFAULT_CREATE_MARKET_GAS_LIMIT ,
217+ ) ;
218+ env. storage ( ) . instance ( ) . set (
219+ & GasConfigKey :: GasLimit ( symbol_short ! ( "claim" ) ) ,
220+ & DEFAULT_CLAIM_WINNINGS_GAS_LIMIT ,
221+ ) ;
222+ }
223+
224+ /// Returns `true` if a gas limit (admin-configured or default) exists
225+ /// for the given operation.
226+ pub fn has_limit ( env : & Env , operation : Symbol ) -> bool {
227+ let ( admin_cpu, _) = Self :: get_limits ( env, operation. clone ( ) ) ;
228+ admin_cpu. is_some ( ) || get_default_limit ( & operation) . is_some ( )
229+ }
230+
231+ /// Retrieves the effective gas limit for an operation, resolving
232+ /// admin-configured override → default regression limit → None.
233+ pub fn get_effective_cpu_limit ( env : & Env , operation : Symbol ) -> Option < u64 > {
234+ let ( admin_cpu, _) = Self :: get_limits ( env, operation. clone ( ) ) ;
235+ admin_cpu. or_else ( || get_default_limit ( & operation) )
236+ }
237+
157238 /// Retrieves the current gas budget limit for an operation.
158239 pub fn get_limits ( env : & Env , operation : Symbol ) -> ( Option < u64 > , Option < u64 > ) {
159240 let cpu = env
@@ -176,22 +257,38 @@ impl GasTracker {
176257
177258 /// Hook to call immediately after an operation.
178259 /// It records usage, publishes an observability event, and checks admin caps.
260+ ///
261+ /// # Regression Limit Enforcement
262+ ///
263+ /// Gas limits are resolved in this priority order:
264+ /// 1. **Admin-configured limit** (via `set_limit`) — checked first.
265+ /// 2. **Default regression limit** (compile-time constant) — used as fallback.
266+ /// 3. **No limit** — operation proceeds unchecked.
267+ ///
268+ /// This means `create_market` and `claim_winnings` are always bounded
269+ /// by their default limits unless an admin explicitly overrides them.
179270 pub fn end_tracking ( env : & Env , operation : Symbol , _start_marker : u64 ) {
180271 let cost = Self :: get_actual_cost ( env, operation. clone ( ) ) ;
181272
182273 // Publish observability event: [ "gas_used", operation ] -> cost
183274 env. events ( )
184275 . publish ( ( symbol_short ! ( "gas_used" ) , operation. clone ( ) ) , cost. clone ( ) ) ;
185276
186- // Optional: admin-set gas budget cap per call (abort if exceeded)
187- let ( cpu_limit, mem_limit) = Self :: get_limits ( env, operation) ;
277+ // Resolve effective limits: admin override > default regression limit.
278+ let ( admin_cpu, admin_mem) = Self :: get_limits ( env, operation. clone ( ) ) ;
279+ let default_limit = get_default_limit ( & operation) ;
280+
281+ // Effective CPU limit: admin-configured takes precedence.
282+ let effective_cpu = admin_cpu. or ( default_limit) ;
283+ // Effective memory limit: admin-configured only (no default for mem).
284+ let effective_mem = admin_mem;
188285
189- if let Some ( limit) = cpu_limit {
286+ if let Some ( limit) = effective_cpu {
190287 if cost. cpu > limit {
191288 panic_with_error ! ( env, crate :: err:: Error :: GasBudgetExceeded ) ;
192289 }
193290 }
194- if let Some ( limit) = mem_limit {
291+ if let Some ( limit) = effective_mem {
195292 if cost. mem > limit {
196293 panic_with_error ! ( env, crate :: err:: Error :: GasBudgetExceeded ) ;
197294 }
@@ -226,8 +323,9 @@ impl GasTracker {
226323 return ;
227324 }
228325
326+ // Use admin-configured limit, falling back to default regression limit.
229327 let ( cpu_limit, _) = Self :: get_limits ( env, operation. clone ( ) ) ;
230- let budget = match cpu_limit {
328+ let budget = match cpu_limit. or_else ( || get_default_limit ( & operation ) ) {
231329 Some ( limit) if limit > 0 => limit,
232330 _ => return , // No budget or zero budget, skip alert
233331 } ;
0 commit comments