This document describes the gas optimization strategies implemented in PredictIQ to ensure the contract stays within Soroban's strict CPU and memory limits.
Markets with thousands of outcomes or winners could exceed Soroban's instruction limits during resolution.
- Outcome Limit: Maximum 100 outcomes per market (
MAX_OUTCOMES_PER_MARKET) - Payout Mode Classification:
Pullis the active distribution path (winners claim individually)Pushis currently a mode flag only; no batch push transfer routine exists
pub const MAX_OUTCOMES_PER_MARKET: u32 = 100;
pub const MAX_PUSH_PAYOUT_WINNERS: u32 = 50;
pub enum PayoutMode {
Push, // Reserved compatibility flag
Pull, // Winners claim individually via claim_winnings
}Large default integers in structs increase ledger footprint costs.
Use Option<T> for fields that may not always be set:
pub struct OracleConfig {
pub oracle_address: Address,
pub feed_id: String,
pub min_responses: Option<u32>, // None defaults to 1
}Benefits:
- Reduces storage size when field is not set
- Saves on ledger entry fees
- More efficient serialization
pub fn resolve_market(e: &Env, market_id: u64, winning_outcome: u32) -> Result<(), ErrorCode> {
let mut market = get_market(e, market_id)?;
market.status = MarketStatus::Resolved;
market.winning_outcome = Some(winning_outcome);
update_market(e, market);
Ok(())
}pub fn resolve_market(e: &Env, market_id: u64, winning_outcome: u32) -> Result<(), ErrorCode> {
let mut market = get_market(e, market_id)?;
// Validate outcome
if winning_outcome >= market.options.len() {
return Err(ErrorCode::InvalidOutcome);
}
// Estimate winner count
let estimated_winners = estimate_winner_count(e, market_id, winning_outcome);
// Select payout mode metadata
if estimated_winners > MAX_PUSH_PAYOUT_WINNERS {
market.payout_mode = PayoutMode::Pull;
} else {
market.payout_mode = PayoutMode::Push;
}
market.status = MarketStatus::Resolved;
market.winning_outcome = Some(winning_outcome);
update_market(e, market);
Ok(())
}New function to monitor gas usage before resolution:
pub struct ResolutionMetrics {
pub winner_count: u32,
pub total_winning_stake: i128,
pub gas_estimate: u64,
}
pub fn get_resolution_metrics(e: &Env, market_id: u64, outcome: u32) -> ResolutionMetricsUsage:
let metrics = contract.get_resolution_metrics(market_id, outcome);
if metrics.gas_estimate > SAFE_THRESHOLD {
// Use pull payouts
}cd contracts/predict-iq/benches
./gas_benchmark.shTests:
- Small market (10 outcomes)
- Medium market (50 outcomes)
- Large market (100 outcomes)
- Multiple bet placement
- Market resolution
- Resolution metrics
cd contracts/predict-iq
cargo test --test gas_benchmark -- --nocaptureTests:
bench_create_market_10_outcomesbench_create_market_50_outcomesbench_create_market_100_outcomesbench_place_multiple_betsbench_resolve_marketbench_get_resolution_metricsbench_reject_excessive_outcomes
| Operation | Outcomes | CPU Instructions | Memory Bytes | Status |
|---|---|---|---|---|
| Create Market | 10 | ~50K | ~2KB | ✓ Safe |
| Create Market | 50 | ~150K | ~8KB | ✓ Safe |
| Create Market | 100 | ~250K | ~15KB | ⚠ Monitor |
| Create Market | 101 | N/A | N/A | ✗ Rejected |
| Place Bet | 1 | ~30K | ~1KB | ✓ Safe |
| Resolve (mode flag set) | 50 winners | ~2.5M | ~50KB | ⚠ Threshold |
| Resolve (mode flag set) | 1000 winners | ~100K | ~5KB | ✓ Safe |
- Limit outcomes to necessary options (prefer <50)
- Consider market size when setting parameters
- Monitor resolution metrics before finalizing
- Set
MAX_OUTCOMES_PER_MARKETbased on network conditions - Adjust
MAX_PUSH_PAYOUT_WINNERSfor mode classification as needed - Monitor instruction counts in production
- Keep UX centered around pull claims (
claim_winnings)
- Always test with maximum outcome counts
- Use
get_resolution_metricsbefore resolution - Implement proper error handling for gas limits
- Consider batching operations when possible
- Average instruction count per operation
- Memory usage per market size
- Resolution time vs winner count
- Failed transactions due to gas limits
Set up monitoring for:
- Markets approaching 100 outcomes
- Markets classified as
Pushthat still require pull claims - Instruction counts exceeding 80% of limits
- Lazy Loading: Load market data in chunks
- Outcome Indexing: Maintain separate indices for faster lookups
- Batch Processing: Process multiple operations in single transaction
- State Compression: Use more efficient data structures
- Winner Tracking: Maintain real-time winner counts during betting
- Zero-knowledge proofs for outcome verification
- Off-chain computation with on-chain verification
- Optimistic rollups for high-volume markets
- Added
PayoutModeenum for push/pull payouts - Implemented
MAX_OUTCOMES_PER_MARKETlimit - Optimized
OracleConfigwithOption<u32> - Added
get_resolution_metricsfunction - Created comprehensive benchmarking suite
- Automatic payout mode classification in
resolve_market