🚧 SportsPerp is currently live on devnet. Mainnet target: before Jun 12, 2026 (World Cup kickoff).
SecuritySelf-Audit Summary

Self-Audit Summary

An internal self-audit of the SportsPerp on-chain program + off-chain services was completed April 17, 2026. The full report is maintained internally and shared with external auditors under engagement. This page summarizes the results.

Headline numbers

Audit dateApril 17, 2026
AuditorInternal (Klaw)
Scope27 Anchor instructions + 7 account types + 6 math modules + 6 off-chain services
Findings total17
Critical1 (fixed)
High5 (all fixed)
Medium4 (documented, non-blocking)
Low4 (documented)
Informational3 (documented)

Status: All Critical and High findings have been resolved and verified with regression tests. The program builds clean (cargo check — 0 errors, 0 warnings) and all 70+ on-chain tests pass.

The Critical finding (fixed)

S4-01: Expired Trigger Order Destroys Position Account

Severity: Critical Fixed: Commits 4a213f2, e1680cc Location: the on-chain trigger-order instruction handler

What it was: The trigger_close instruction used Anchor’s close account constraint on the user’s position account. When a Stop-Loss or Take-Profit order expired, the handler returned early — but Anchor’s close constraint executed unconditionally during account deserialization, destroying the user’s position account and transferring its rent to the keeper. Result: any expired SL/TP would silently delete the user’s open position.

Impact: Loss of user position state. Position would need to be reconstructed off-chain; collateral would be recoverable but the active position would effectively disappear.

Fix:

  • Removed the unconditional Anchor close constraint from the position account.
  • Manual position.close() now executes only after successful order execution.
  • The expired-order path leaves the position intact, only closing the trigger order PDA.
  • Extended fix to LimitOpen — expired limit orders now explicitly close ghost PDAs and refund collateral escrow to the user.

Regression tests: the S4-01 expiry regression suite covers 5 scenarios:

  1. Expired StopLoss — trigger closed, position survives.
  2. Expired TakeProfit — trigger closed, position survives.
  3. Expired LimitOpen — trigger closed, collateral refunded, no ghost PDA.
  4. Non-expired orders still execute normally.
  5. Collateral refund verification for expired LimitOpen.

The 5 High-severity findings (all fixed)

S2-03: Backstop Liquidation Skipped Funding Payment

Layer 2 backstop absorption computed payout without first deducting accumulated funding. A position owing significant funding could extract more from the insurance fund than warranted.

Fix: Funding payment is now calculated and deducted before any backstop payout math. Commit 4a213f2.

S2-04: ADL Skipped Proportional Funding

Auto-deleveraging force-closes profitable opposing positions but did not apply proportional funding to the target’s payout. A target with large accumulated funding obligations could receive a full PnL payout without settling funding debt.

Fix: Funding is now calculated and deducted proportionally before the deleveraged payout. Commit 4a213f2.

S3-02: Market Settlement Skipped Funding

When a market is sunset (oracle stale ≥ 72h) and positions are settled at the settlement price, settle_position did not deduct accumulated funding before computing the final payout.

Fix: Funding is now deducted before settlement payout computation. Commit 4a213f2.

S3-01: Missing Vault Balance Pre-Checks

Four instruction handlers that transfer SOL/USDC from the vault lacked explicit vault.amount >= payout checks. Solana would eventually reject an insufficient-funds transfer, but the lack of explicit checks meant wasted gas on failed transactions and opaque error messages.

Affected handlers: close_position, settle_position, backstop_liquidate, trigger_close.

Fix: Added explicit require!(vault.amount >= payout, OBVPerpsError::InsufficientVaultBalance) checks before every transfer. Commit 4a213f2.

S1-02: Insurance Fund Could Be Fully Drained

The withdraw_insurance admin instruction did not enforce a floor on the fund’s balance. An admin (or a compromised admin keypair) could drain the full fund balance at any time, leaving no capital to absorb Layer 2 events.

Fix: withdraw_insurance now enforces a floor of max(120% × current_backstop_exposure, 20% × target_balance). The exposure-linked term guarantees the fund always retains enough capital to cover outstanding Layer 2 obligations plus a 20% buffer; the target-linked term prevents a complete drain even when no positions are absorbed. Admin cannot go below either threshold, regardless of signing authority.

Medium-severity findings (documented)

Four findings at Medium severity — documented but not blocking. These generally involve edge cases that are unlikely in normal operation but worth noting for integrators:

  • Partial funding settlement during transfer ops — add_collateral and withdraw_collateral settle pending funding but within a narrow timing window. In theory a very aggressive caller could sequence operations to marginally delay funding payment. In practice, the timing is within one block.
  • Oracle update races with liquidation cascade — a price update landing simultaneously with a liquidation can result in the liquidation executing against the pre-update mark. Expected behavior, but documented.
  • Backstop unwind reward rounding — at very small unwind sizes, the 3% reward rounds to zero. Practically irrelevant but noted.
  • Trigger order size validation — a trigger can be placed with a size larger than the current position size; at execution time the size is capped at the current position size. Not a bug, but could be confusing; clearer error messaging planned.

Low and Informational findings

Documented in the full report. Primarily code-quality observations (redundant checks, opportunities for clearer error codes, optional efficiency improvements) that don’t affect security or correctness. None block mainnet.

Cross-audit invariants verified

Beyond the individual findings, the self-audit verified these invariants end-to-end:

  • Vault balance never becomes negative. Enforced across the liquidation cascade, sunset settlement, and trigger execution.
  • Insurance fund balance never exceeds total_deposits - total_withdrawals — no implicit emission.
  • Every position that can be opened can be closed. No dead-end states.
  • Every trigger order is either executable, cancellable, or expirable — no stuck orders.
  • PnL formula is symmetric across long and short. Same rounding bias, same direction.
  • Cumulative funding counters are monotonic. A position’s entry snapshot + later settlement always produces a non-negative unit payment delta for the obligated side.

These invariants are additionally exercised by the Phase 5 integration test suite (27 tests, 9 risk scenarios).

What the self-audit did not cover

A self-audit is valuable but has inherent limits: the same team that wrote the code reviewed it, so familiarity and shared-assumption bias are not fully caught, and an independent specialist firm with a dedicated adversarial mandate will explore paths an internal pass does not. That is precisely why an external audit by Solana-specialist firms is a hard mainnet-blocking gate — the self-audit is a floor, not a substitute.

The full report

The canonical self-audit report records every finding with a full description, the fix’s commit history, regression-test references, and a residual-risk assessment. It is maintained internally and provided to the external audit firms under engagement; relevant resolved findings are also reflected in this public summary.

Further reading