Skip to content

Solana Pre-flight Risk and Submission

Overview

Oracle3 applies two risk layers before recording a live Solana trade. The local layer evaluates portfolio limits before a transaction is requested, while the on-chain layer simulates the signed transaction with Solana JSON-RPC immediately before submission. The simulation verifies whether the transaction would execute without a runtime error; it does not replace the local position, exposure, drawdown, or loss controls.

Component diagram

graph LR
    Strategy --> TradingEngine
    TradingEngine --> SolanaTrader
    SolanaTrader --> RiskManager
    RiskManager -. wrapped by .-> OnChainRiskManager
    SolanaTrader --> OnChainRiskManager
    OnChainRiskManager --> SolanaRPC[Solana RPC]
    SolanaTrader --> JitoSubmitter
    JitoSubmitter --> SolanaRPC

TradingEngine provides the event loop and binds the strategy to the trader. When the strategy places an order, SolanaTrader coordinates both risk layers: it calls RiskManager.check_trade() before requesting a transaction, then calls OnChainRiskManager.simulate_transaction() after signing it and before either Jito or standard RPC submission.

Local risk checks

OnChainRiskManager wraps StandardRiskManager and delegates its first decision to StandardRiskManager.check_trade() (oracle3/risk/onchain_risk_manager.py:86). The local checks run in sequence and reject the trade as soon as one fails:

Guard Behavior Source
Maximum single trade Rejects when quantity * price exceeds max_single_trade_size. oracle3/risk/risk_manager.py:89
Position limit Rejects a buy that would exceed max_position_size for the ticker; sells reduce the position and pass this guard. oracle3/risk/risk_manager.py:94
Total exposure Excludes cash from current exposure and rejects a buy that would exceed max_total_exposure. oracle3/risk/risk_manager.py:115
Drawdown Tracks peak portfolio value and rejects when current drawdown reaches the configured maximum. oracle3/risk/risk_manager.py:134
Daily loss When configured, rejects after portfolio loss exceeds daily_loss_limit. oracle3/risk/risk_manager.py:156
Open positions Rejects a new buy when max_positions is reached, while allowing sells and additions to existing positions. oracle3/risk/risk_manager.py:169

SolanaTrader.place_order() also performs order-shape, short-selling, and available-cash checks before the risk manager is called (oracle3/trader/solana_trader.py:343).

On-chain pre-flight

After the DFlow Trade API returns a serialized transaction, SolanaTrader deserializes and signs it. If its risk manager is an OnChainRiskManager, the trader sends the signed bytes to simulate_transaction() before submission (oracle3/trader/solana_trader.py:171).

The method base64-encodes the signed transaction and sends this JSON-RPC request (oracle3/risk/onchain_risk_manager.py:110):

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "simulateTransaction",
  "params": [
    "<base64 signed transaction>",
    {
      "encoding": "base64",
      "commitment": "confirmed",
      "replaceRecentBlockhash": true
    }
  ]
}

Oracle3 reads result.value.err and result.value.logs from the response. A non-null err rejects the transaction and prevents submission. A null err permits submission, and the log count is written only at debug level. The current implementation does not independently enforce compute-unit limits, inspect account writes, or calculate slippage from the simulation response; those concerns must be handled by transaction construction, local risk rules, or upstream APIs.

MEV protection

When Jito support is enabled, SolanaTrader passes the validated signed transaction to JitoSubmitter.submit_with_jito() (oracle3/trader/jito_submitter.py:70). The submitter creates a two-transaction bundle containing the trade and a SOL tip, then sends it to the Jito Block Engine. If Jito rejects the bundle or the request fails, the submitter falls back to Solana sendTransaction with RPC pre-flight enabled (skipPreflight: false) (oracle3/trader/jito_submitter.py:200).

After either Jito or standard RPC returns a signature, SolanaTrader polls getSignatureStatuses until the transaction is confirmed, finalized, failed, or times out (oracle3/trader/solana_trader.py:211). Positions are updated only after confirmation succeeds.

Failure modes

Failure Current behavior
Simulation RPC timeout, transport error, or malformed response simulate_transaction() catches the exception and fails open, allowing submission to continue. The later submission and confirmation steps can still reject the trade.
BlockhashNotFound or another simulation error The error appears in result.value.err, so simulation returns False and SolanaTrader blocks submission. replaceRecentBlockhash: true reduces stale-blockhash failures during simulation but does not guarantee later inclusion.
Simulation succeeds but submission RPC rejects Standard submission raises an error; Jito submission falls back to standard RPC. The order returns an unknown failure and no position is recorded.
Submission returns a signature but execution fails getSignatureStatuses reports a non-null err; confirmation returns False and no position is recorded.
Confirmation does not arrive within 30 seconds Confirmation returns False; the order is treated as failed even though final network outcome may require separate reconciliation.