This issue requires implementing idempotency for event creation to prevent charging duplicate event-creation fees on retry scenarios. When an admin creates an event and encounters a network timeout, a retry should not result in a second fee charge.
- Entry point:
create_eventincontracts/predictify-hybrid/src/lib.rs:773-873 - Current operations (in order):
- Circuit breaker check
- Auth check (primary admin)
- Rate limiting
- Input validation (outcomes count, description, oracle config)
- Event ID generation (via MarketIdGenerator)
- Event storage
- Event emission
- Statistics recording
- Audit trail recording
- Gas tracking
- Current: Fee collection is NOT implemented in
create_event, despite tests expecting it - Reference implementation:
process_creation_feeexists infees.rs:779and is used increate_market(markets.rs:132) - Test expectations: Tests in
test.rsexpect fees to be charged:test_create_event_collects_configured_fee_and_emits_event(line 2418)test_create_event_rejects_when_fee_insufficient(line 2491)test_create_event_rejects_when_fee_asset_not_configured(line 2532)test_create_event_uses_configured_fee_asset(line 2568)
Located in place_bets_idempotency_tests.rs and bets.rs:
Storage Key Design:
DataKey::PlaceBetsIdem(Address, BytesN<32>)Characteristics:
- Scope: User-specific (keyed by caller Address + BytesN<32>)
- Key Derivation: Caller-supplied 32-byte key (required parameter)
- TTL:
PLACE_BETS_IDEM_TTL_LEDGERS(~7 days at 5s/ledger = ~525k ledgers) - Behavior on Retry: Returns
IdempotentBatchAlreadyAppliederror - Entry Point:
place_betsfunction inbets.rs
Key Insights:
- Keys are caller-supplied → requires client coordination
- Keys are address-scoped → same raw bytes can be reused by different users
- TTL-based expiry → keys are automatically cleaned up after TTL window
- Deterministic error on replay → clients receive clear signal
Located in storage.rs:
DataKey Enum:
pub enum DataKey {
PlaceBetsIdem(Address, soroban_sdk::BytesN<32>),
EventNonce(Symbol),
AdminOverrideNonce,
// ... many others
}TTL Constants:
pub const PLACE_BETS_IDEM_TTL_LEDGERS: u32 = 7 * LEDGERS_PER_DAY; // ~7 days
pub const EVENT_TTL_LEDGERS: u32 = 90 * LEDGERS_PER_DAY; // ~90 daysFollowing the pattern of place_bets, we will implement caller-supplied idempotency keys for create_event:
- Consistency: Reuses proven pattern from
place_bets - Flexibility: Allows clients to coordinate retries intelligently
- Safety: Caller explicitly manages retry semantics
- Scope: Admin-scoped keys prevent collision across different admins
- Minimally Invasive: Adds optional parameter to existing function
- Storage Key:
DataKey::CreateEventIdem(Address, BytesN<32>) - Key Scope: Admin-specific (keyed by admin Address + caller-supplied 32-byte key)
- TTL:
CREATE_EVENT_IDEM_TTL_LEDGERS=PLACE_BETS_IDEM_TTL_LEDGERS(~7 days) - Behavior on Duplicate: Return
IdempotentBatchAlreadyAppliederror before fee collection - Return on Success: Return the cached event_id if already processed
- Parameter: Add optional
idempotency_key: Option<BytesN<32>>tocreate_event
- Add
CreateEventIdem(Address, BytesN<32>)variant toDataKeyenum instorage.rs - Add
CREATE_EVENT_IDEM_TTL_LEDGERSconstant (reusePLACE_BETS_IDEM_TTL_LEDGERS)
- Modify
create_eventfunction signature to accept optional idempotency key - Add idempotency check BEFORE any state mutation (fee collection, event storage, etc.)
- On cache hit: return cached event_id with error if key already consumed
- On cache miss: proceed with normal flow
- On success: store idempotency key with event_id value and TTL
- Call
process_creation_feeAFTER idempotency check succeeds - Ensures fee is charged atomically with event creation
- On duplicate: fees are NOT collected (idempotency check fires first)
- Happy path: Fresh key succeeds
- Duplicate key: Returns
IdempotentBatchAlreadyAppliedwithout charging fee - Different key: Succeeds independently (same admin)
- Key scope: Same key bytes, different admin → independent calls
- TTL expiry: After TTL, key reusable
- Integration: Fees charged exactly once per unique event
| Criterion | Implementation | Verification |
|---|---|---|
| Deterministic behavior (valid/invalid/duplicate) | Idempotency key check + error codes | Tests for each scenario |
| Authorization/validation enforcement | Idempotency check AFTER auth/validation gates | Existing checks + integration tests |
| Retry/partial failure safety | Check BEFORE state mutation + atomic TTL store | Retry tests + fee verification |
| Concurrent execution safety | Soroban storage atomicity + key scoping | Concurrency tests if needed |
| Test coverage (success/rejection/boundary/regression) | Comprehensive test suite below | Test matrix |
| Backward compatibility | Optional parameter with sensible default | Works without key parameter |
| Observability | Clear error codes + audit trail entries | Error messages + audit logs |
Consider adding: IdempotentEventAlreadyCreated or reuse IdempotentBatchAlreadyApplied
Rationale for Reuse:
- Both represent "idempotent operation already applied"
- Reduces error enum bloat
- Consistent with place_bets pattern
- Clients can handle identically
- Circuit breaker error → panic
- Auth error → panic
- Rate limit error → panic
- Validation error (outcomes, description, oracle) → panic
- Idempotency check → panic with error (NEW)
- Fee collection error → panic
- Storage error → panic
- Event creation can be called multiple times with identical inputs → multiple fees charged
-
If idempotency key is provided:
- First call with key K → event created, fee charged, key stored with event_id value
- Second call with key K (within TTL) →
IdempotentBatchAlreadyAppliederror, no fee - After TTL expiry → key can be reused (new event creation)
-
If no idempotency key provided:
- Backward compatible: call proceeds (optional parameter)
- Risk of duplicate fees remains for this call
- Future migration can make key mandatory
-
Key stored, then fee fails: Idempotency check fires on retry, no fee charged ✓
-
Fee collected, then key storage fails: Retry sees fee already charged (inconsistent) ✗
- Mitigation: Store key FIRST (before fee), or use transaction-like semantics
- Soroban Consideration: No transactions; use storage order carefully
-
Network timeout during event storage: Admin can retry safely with same key ✓
- Order: Check idempotency → charge fee → store event → store idempotency key
- Rationale: If fee fails, admin can retry and will pay again (acceptable)
- Alternative: Store idempotency key first → charge fee → check didn't lose key
- Chosen: First approach (fee then key) matches
place_betspattern where key is checked first
- Per event creation: +1 persistent entry (idempotency key)
- Size: ~104 bytes (Address + BytesN<32> + event_id Symbol + metadata)
- TTL: ~7 days, then auto-expired
- Worst case: ~10k events/week × 104 bytes = ~1MB/week (manageable)
idempotency_key: Option<BytesN<32>>parameter added tocreate_event- Existing callers that don't provide key: still work (backward compatible)
- New callers: can provide key for deduplication safety
- Could make key mandatory in future version (new entrypoint or upgrade)
- Could auto-derive key from (admin, description, outcomes, end_time) for zero-touch idempotency
- Current approach gives flexibility for rollout
-
Happy Path
- Fresh key succeeds, event created, fee charged ✓
- Event id returned correctly
-
Duplicate Detection
- Same key, same admin →
IdempotentBatchAlreadyApplied(no fee) - Verify no double-charge via token balance checks
- Same key, same admin →
-
Key Scope
- Admin A with key K succeeds
- Admin B with same key K bytes → succeeds (different scope)
- Same admin, different key → succeeds independently
-
TTL Expiry
- Create event with key K → event created
- Advance ledgers past TTL
- Reuse key K → succeeds as fresh creation
-
Boundary Cases
- Null/empty key → should reject or handle gracefully
- Key collision (internal): near-impossible with 256-bit key
-
Integration
- Fee collection happens exactly once
- Audit trail records event creation correctly
- Event appears in storage on first call only
-
Regression
- All existing event creation tests still pass
- No accidental changes to event schema or behavior