Skip to main content

Trading Contract

The trading contract is the core perpetual futures engine. It manages position lifecycle, PnL settlement, fee distribution, funding rate accrual, liquidation, and auto-deleveraging.

Public Interface

User Actions

All user actions require authentication from the position owner. The one exception is cancel_position against a filled position whose market has been deleted, which is permissionless cleanup so stranded positions can be refunded by anyone.

FunctionStatus RequiredDescription
place_limitActivePlace a limit order (pending fill)
open_marketActiveOpen a position at market price
close_positionNot FrozenClose a filled position
cancel_positionAnyCancel a pending limit order, or refund a filled position on a deleted market
modify_collateralNot FrozenAdd or remove collateral
set_triggersNot FrozenSet stop-loss and take-profit prices

Keeper Actions

Keeper actions are permissionless and earn fees on each filled or triggered position in the batch.

FunctionDescription
executeBatch process: fills, SL/TP triggers, liquidations. Caller earns caller_rate × trading_fee per processed position.

Permissionless Maintenance

Anyone can call these. They are not paid. In the canonical Zenith deployment a protocol-operated Guardian bot calls them on schedule, but the on-chain functions accept any caller.

FunctionDescription
apply_fundingUpdate funding and borrowing accrual indices across all markets. Rate-limited to one call per hour.
update_statusRe-evaluate global PnL against vault size and transition between Active and OnIce, or trigger ADL when conditions are met.

Admin Actions

All admin actions require the contract owner (#[only_owner]).

FunctionDescription
set_configUpdate global trading configuration
set_marketAdd or update a market
del_marketRemove a market (existing positions can be refunded via cancel_position)
set_statusSet contract status (cannot set OnIce, use update_status instead)
upgradeUpgrade contract WASM
transfer_ownershipTransfer admin rights (OZ Ownable)

Read-Only

FunctionDescription
get_configCurrent TradingConfig
get_market_configMarketConfig for a market ID
get_market_dataMarketData for a market ID
get_marketsAll registered market IDs
get_positionPosition for (user, id) pair
get_user_counterPer-user monotonic sequence number, also equals total positions ever created for that user
get_statusCurrent contract status
get_treasuryTreasury contract address
get_vaultVault contract address
get_price_verifierPrice verifier contract address
get_tokenCollateral token address

The contract does not enumerate a user's open positions on chain. Two paths to list them:

  • Off-chain indexer. Replay the position-lifecycle events (PlaceLimit, OpenMarket, close/refund) and keep a per-user index. Scales with active position count and is the right choice for production frontends.
  • Direct ledger probe. get_user_counter returns N, the total positions ever created by the user. Batch-fetch Position(user, 0) through Position(user, N-1) via Soroban's getLedgerEntries RPC. Closed and refunded positions leave no storage entry, so any returned key is an active position. Scales with lifetime count, which gets expensive for traders with long history. Useful for fully on-chain readers and as a fallback when indexing is unavailable.

Data Structures

TradingConfig

Global fee, rate, and limit parameters set by the admin via set_config.

FieldTypeDescription
caller_ratei128 (SCALAR_7)Keeper's share of trading fees (0 to 50%)
min_notionali128 (token decimals)Minimum notional size per position
max_notionali128 (token decimals)Maximum notional size per position
fee_domi128 (SCALAR_7)Trading fee rate for the dominant side (heavier open interest)
fee_non_domi128 (SCALAR_7)Trading fee rate for the minority side
max_utili128 (SCALAR_7)Global utilization cap: total_notional / vault_balance
r_fundingi128 (SCALAR_18)Base hourly funding rate (all markets)
r_basei128 (SCALAR_18)Base hourly borrowing rate (all markets)
r_vari128 (SCALAR_18)Vault-level variable borrowing rate at full vault utilization

caller_rate in [0, MAX_CALLER_RATE] (50%). fee_dom >= fee_non_dom >= 0, both <= MAX_FEE_RATE (1%). r_base, r_funding in [0, MAX_RATE_HOURLY] (0.01%/hr, ~88% APR). r_var in [0, MAX_R_VAR] (0.01%/hr). min_notional > 0, max_notional > min_notional, max_util > 0.

MarketConfig

Per-market parameters set by the admin via set_market.

FieldTypeDescription
feed_idu32Price feed identifier (immutable after market creation)
enabledboolWhether this market accepts new positions
max_utili128 (SCALAR_7)Per-market utilization cap
r_var_marketi128 (SCALAR_18)Per-market variable borrowing rate at full market utilization
margini128 (SCALAR_7)Initial margin ratio. Max leverage = 1 / margin
liq_feei128 (SCALAR_7)Liquidation fee/threshold. Position liquidatable when equity < notional * liq_fee
impacti128 (SCALAR_7)Price impact fee divisor: fee = notional / impact

margin > liq_fee > 0. margin <= MAX_MARGIN (50%). liq_fee <= MAX_LIQ_FEE (25%). r_var_market in [0, MAX_R_VAR_MARKET] (0.01%/hr). impact >= MIN_IMPACT (10x). max_util > 0.

MarketData

Per-market mutable state, updated on every position action.

FieldTypeDescription
l_notionali128Sum of all long notional sizes (token decimals)
s_notionali128Sum of all short notional sizes (token decimals)
l_fund_idxi128 (SCALAR_18)Cumulative long funding index
s_fund_idxi128 (SCALAR_18)Cumulative short funding index
l_borr_idxi128 (SCALAR_18)Cumulative long borrowing index
s_borr_idxi128 (SCALAR_18)Cumulative short borrowing index
l_entry_wti128sum(notional_i / entry_price_i) for longs
s_entry_wti128sum(notional_i / entry_price_i) for shorts
fund_ratei128 (SCALAR_18)Current signed funding rate (positive = longs pay)
last_updateu64Timestamp of last accrual (seconds)
l_adl_idxi128 (SCALAR_18)Long ADL reduction factor (starts at SCALAR_18)
s_adl_idxi128 (SCALAR_18)Short ADL reduction factor (starts at SCALAR_18)

The entry_wt fields enable aggregate PnL computation without iterating all positions. They are used by the circuit breaker and ADL system. The borr_idx fields track cumulative borrowing costs per unit of notional, accrued alongside funding.

Position

The owner is not stored on the position struct itself. It is encoded in the storage key Position(Address, u32). Position IDs come from UserCounter(Address), so two different users can both hold positions with id 0. The (user, id) pair is what uniquely identifies a position.

FieldTypeDescription
filledboolfalse = pending limit order, true = active position
market_idu32Market identifier (maps to MarketConfig with feed_id)
longboolDirection
sli128Stop-loss trigger price (0 = disabled)
tpi128Take-profit trigger price (0 = disabled)
entry_pricei128Fill price (price decimals)
coli128Current collateral (token decimals)
notionali128Notional value, may be reduced by ADL (token decimals)
fund_idxi128 (SCALAR_18)Funding index snapshot at fill
borr_idxi128 (SCALAR_18)Borrowing index snapshot at fill
adl_idxi128 (SCALAR_18)ADL index snapshot at fill (starts at SCALAR_18)
created_atu64Timestamp of creation or fill (seconds)

Contract Status System

Active (0)      : Normal operation, all actions allowed
OnIce (1) : Permissionless circuit breaker; no new positions
AdminOnIce (2) : Admin-set pause; same restrictions as OnIce
Frozen (3) : Full freeze; all operations blocked
StatusOpen positionManage positionKeeper executeApply funding
ActiveYesYesYesYes
OnIceNoYesYesYes
AdminOnIceNoYesYesYes
FrozenNoNoNoYes

apply_funding has no status check at the entrypoint, so it stays callable even when the contract is Frozen. This keeps index accrual ticking through an emergency pause so positions resume from accurate state once the freeze is lifted.

Admin can set Active, AdminOnIce, or Frozen directly. OnIce can only be set by the permissionless update_status function when utilization thresholds are met.

Constants

All rates, ratios, and indices are stored as fixed-point integers. SCALAR_7 = 10^7 is the base for fees, ratios, and utilization. SCALAR_18 = 10^18 is the base for funding rates, borrowing rates, and cumulative indices. The values below are the protocol bounds and operational constants the contract enforces or relies on.

ConstantValueDescription
MAX_ENTRIES50Maximum number of registered markets
UTIL_ONICE9_500_00095%. Triggers OnIce when net PnL >= 95% of vault
UTIL_ACTIVE9_000_00090%. Restores Active when PnL drops below 90%
ONE_HOUR_SECONDS3600Funding/borrowing update minimum interval
MIN_OPEN_TIME30Minimum seconds before user-initiated close
MAX_CALLER_RATE5_000_00050% max keeper fee share
MAX_FEE_RATE100_0001% max base fee rate
MAX_RATE_HOURLY10^140.01%/hr max for r_base, r_funding (~88% APR)
MAX_R_VAR10^140.01%/hr max vault variable rate
MAX_R_VAR_MARKET10^140.01%/hr max per-market variable rate
MAX_UTIL100_000_0001000% max utilization cap (10x SCALAR_7)
MIN_IMPACT100_000_000Impact divisor floor (caps impact fee at 10%)
MAX_MARGIN5_000_00050% max initial margin (2x min leverage)
MAX_LIQ_FEE2_500_00025% max liquidation fee/threshold