Market Open Fee on Execution Price

Market open trading fees were charged on the market price supplied at request time. They are now charged on the price PartyB actually supplied for the fill.

A market quote carries two prices. marketPrice is the request-time spot price in PartyA's UPNL payload, and openedPrice is the price PartyB filled at. Until this change the open trading fee was computed from marketPrice. Close fees already used the executed closedPrice, so open fees were the only trading fee in the protocol still priced off a request-time input.

Why request-time price was unsafe

marketPrice reaches storage from the uPnL payload submitted with PartyA's request. On the normal path a Muon signature covers that payload, so the value is attested. When PartyA is bound to a PartyB, sendQuote skips Muon verification because binding is the oracle-less path. The request-time price is accepted as supplied.

A bound PartyA could therefore submit a market quote with marketPrice set to zero. The quote was otherwise valid: requestedOpenPrice, quantity, and locked values were all normal, and every price guard on the open path compares openedPrice against requestedOpenPrice, never against marketPrice. Only the fee read the request-time value.

openFee = quantity * marketPrice * tradingFee / 1e36
        = quantity * 0            * tradingFee / 1e36
        = 0

The position opened normally and the fee collector received nothing. Nothing on the open path detected this, because nothing on the open path had a reason to look at marketPrice.

Charging on openedPrice removes PartyA's request-time marketPrice from the executed fee calculation. PartyB supplies openedPrice at fill time, and the protocol bounds it against requestedOpenPrice in the required direction. It must be at or below the requested price for a long and at or above it for a short. The collector's fee follows the price recorded for the opened position.

Reserved and executed fees

The fee is now expressed as two bases over the same formula, differing only in which price they read.

  1. Reserved uses requestedOpenPrice for limit quotes and marketPrice for market quotes. This is what sendQuote debits from allocated balance and mirrors into partyAReservedOpenFees.
  2. Executed uses requestedOpenPrice for limit quotes and openedPrice for market quotes. This is what the fee collector receives.

Limit quotes read the same price on both sides, so their two bases are always equal and no adjustment ever occurs. The split is only observable for market orders.

Fee lifecycle

An unopened quote has no execution price, so the reserved basis is the only one that exists for a pending or locked quote. The reservation stays on that basis for its whole life, and the difference is settled once, at open.

  1. sendQuote debits the reserved fee from allocated balance and records it as a reservation.
  2. openPosition writes openedPrice, then moves only the difference between reserved and executed on allocated balance. A shortfall is debited as PLATFORM_FEE_OUT; an excess is credited back as PLATFORM_FEE_IN.
  3. The reservation is released against the reserved amount, and the fee collector is credited the executed amount.

PartyA has then paid exactly the executed fee, and the reservation ledger has unwound exactly what it recorded. The difference is applied before the caller verifies solvency, so a shortfall PartyA cannot afford reverts the entire open rather than leaving the account short.

What did not change

Every path that hands an open fee back operates on a quote that never opened, so all of them keep using the reserved basis and return exactly what sendQuote took:

  1. Cancelling or expiring a pending quote, and force-cancelling a locked one.
  2. PartyB unlocking a quote it had locked.
  3. The unfilled remainder of a partially filled limit quote that is cancelled.
  4. PartyA and PartyB liquidation of pending quotes, including clearing-house takeover and escrow accumulation.
  5. Reservations rebuilt by migrateQuotes.

Limit orders are unaffected end to end. Partial fills are unaffected, because a market quote must be filled in full and so never takes the partial-fill path.

Integration notes

  1. TradingFeeCharged with TradingFeeType.OPEN now carries the executed fee. Indexers that recomputed the fee from marketPrice will disagree with the event for market orders and should read the event value.
  2. onFeeCharged receives the same executed amount on both the affiliate and the system hook, so affiliate revenue accounting matches what the collector received.
  3. Opening a market position can now emit a BalanceChangePartyA event for PartyA. Consumers that assumed openPosition never moved allocated balance for PartyA need to account for the adjustment.
  4. A market open can revert on the fee where it previously could not, when the executed price is high enough that PartyA cannot cover the shortfall. Solvers should size fills against the executed price rather than the request-time one.