Skip to main content

Storage & Events

Storage Layout

All storage keys are defined in TradingStorageKey. Storage is split into three TTL tiers.

Instance Storage (30-day TTL)

Global state that is accessed frequently and shared across all calls.

KeyTypeDescription
Statusu32Contract status enum value
VaultAddressVault contract address
TokenAddressCollateral token address
PriceVerifierAddressPyth Lazer verifier address
TreasuryAddressProtocol fee recipient
ConfigTradingConfigGlobal trading parameters
TotalNotionali128Sum of all position notionals across all markets
LastFundingUpdateu64Timestamp of last apply_funding call

Persistent Storage: Market Tier (45/52-day TTL)

Per-market data, the global market list, and per-user position-id counters. The user counter is bumped at the market tier so it survives even if all of a user's positions expire, which prevents id reuse.

KeyTypeDescription
MarketsVec<u32>List of registered market IDs (max MAX_ENTRIES)
MarketConfig(u32)MarketConfigPer-market parameters
MarketData(u32)MarketDataPer-market mutable state
UserCounter(Address)u32Per-user monotonic position-id sequence (next id to allocate)

Persistent Storage: Position Tier (14/21-day TTL)

Per-position data. Shorter TTL because perp positions are short-lived (most close within days).

KeyTypeDescription
Position(Address, u32)PositionIndividual position data, keyed by (owner, per-user id)

Each user has their own counter (UserCounter(Address)), so two different users can both hold positions with id 0. The (user, id) pair is the unique on-chain identifier. The Position struct itself does not carry a user field; the owner is encoded in the storage key.

UserCounter is never decremented. Closing a position does not free its id for reuse; this simplifies event indexing and prevents id collisions across the lifetime of a user's account.

There is no on-chain enumeration of a user's open positions: discovery requires off-chain indexing of position-lifecycle events. The contract exposes get_user_counter(user) -> u32 (the next sequence number, not a list) and get_position(user, id) -> Position.

TTL Strategy

TierThresholdBumpRationale
Instance30 days31 daysAccessed on every call; minimal expiry risk
Market Persistent45 days52 daysMarket config/data and market list; moderate access frequency
Position Persistent14 days21 daysPer-position records; short-lived data

All TTLs are bumped on read or write. If a position is not touched for 14+ days, its Position(user, id) record could expire. Positions are short-lived (most close within days), so the shorter TTL avoids paying rent for abandoned positions. The UserCounter(Address) entry lives at the longer market tier (45/52 days), so the counter survives even when the user's positions are pruned, preventing id reuse.

Events

All events use Soroban's #[contractevent] derive macro. Fields marked with #[topic] are indexed for efficient filtering.

Admin Events

EventTopicsData
SetConfigNone(no data)
SetMarketmarket_idNone
SetStatusNonestatus: u32

Position Events

Each position-lifecycle event carries only the fields that change at the emit moment. Off-chain indexers combine each event with the position row they already hold; fields established by an earlier event (e.g. long, col, notional set at PlaceLimit) are not repeated on later events.

EventTopicsData
PlaceLimitmarket_id, user, position_idlong, col, notional, entry_price, sl, tp, created_at
OpenMarketmarket_id, user, position_idlong, col, notional, entry_price, sl, tp, fund_idx, borr_idx, adl_idx, created_at, base_fee, impact_fee
FillLimitmarket_id, user, position_identry_price, fund_idx, borr_idx, adl_idx, created_at, base_fee, impact_fee
ClosePositionmarket_id, user, position_idnotional, price, pnl, base_fee, impact_fee, funding, borrowing_fee
TakeProfitmarket_id, user, position_idnotional, price, pnl, base_fee, impact_fee, funding, borrowing_fee
StopLossmarket_id, user, position_idnotional, price, pnl, base_fee, impact_fee, funding, borrowing_fee
Liquidationmarket_id, user, position_idnotional, price, base_fee, impact_fee, funding, borrowing_fee, liq_fee
RefundPositionmarket_id, user, position_id(no data)
ModifyCollateralmarket_id, user, position_idcol (new total collateral after modification, not a delta)
SetTriggersmarket_id, user, position_idsl, tp

Notes:

  • On ClosePosition, TakeProfit, StopLoss, and Liquidation, the notional field is the post-ADL notional actually settled. Traders that have been auto-deleveraged will see a smaller notional on the settlement event than on the original OpenMarket / FillLimit.
  • RefundPosition carries no data. Indexers that need the refund amount must read it from the prior position state, or use the return value of cancel_position directly (the refund amount is returned by the call).
  • ModifyCollateral exposes col (post-modification total). To compute the delta, indexers look up the prior position.
  • SetTriggers field order is sl, tp (the on-chain field names).

Market Events

EventTopicsData
DelMarketmarket_id(no data)

System Events

EventTopicsData
ApplyFundingNone(no data)
ADLTriggeredNonereduction_pct, deficit

Close events include borrowing_fee as a separate field alongside base_fee, impact_fee, and funding. The emitted pnl is the net PnL (after all fees, clamped to -col). The notional data field on close/liquidation events is the post-ADL value at settlement.

Error Codes

All errors use panic_with_error!(e, TradingError::Variant). Errors are hard panics that abort the entire transaction, including keeper batch execution via execute (which returns ()).

CodeNameDescription
1UnauthorizedNon-owner tried owner-only action
700InvalidConfigConfig parameter out of valid range
701MarketNotFoundNo market registered for the given market_id
702MarketDisabledMarket is disabled or deleted
703MaxMarketsReachedMAX_ENTRIES markets already registered
710InvalidPricePrice verification failed, market_id mismatch, or missing feed
711StalePricePrice data predates position open time
712PriceSlippageFill price outside the user-supplied price_bound
720PositionNotFoundPosition ID not found in storage
721PositionNotPendingPosition is filled; expected pending
723NegativeValueNotAllowedA parameter is zero or negative
724NotionalBelowMinimumBelow min_notional
725NotionalAboveMaximumAbove max_notional
726LeverageAboveMaximumExceeds 1/margin
727CollateralUnchangedModify to same value
728WithdrawalBreaksMarginWithdrawal would breach initial margin
731NotActionableNo valid action for this position in execute batch
732PositionTooNewMIN_OPEN_TIME not elapsed
733ActionNotAllowedForStatusAction not allowed for position status
734InvalidInputMalformed input (e.g. execute users/ids vec length mismatch)
740InvalidStatusInvalid or disallowed contract status value
741ContractOnIceNew positions blocked (OnIce, AdminOnIce, or Frozen)
742ContractFrozenPosition management blocked (Frozen). cancel_position is exempt; collateral refunds are not held hostage by a freeze.
750ThresholdNotMetNet PnL below ADL threshold
751UtilizationExceededPosition would exceed notional/vault cap
752FundingTooEarlyapply_funding called < 1 hour since last call
760ExpiredCurrent ledger is past the user-supplied expiration_ledger