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.
-
Reserved uses
requestedOpenPricefor limit quotes andmarketPricefor market quotes. This is whatsendQuotedebits from allocated balance and mirrors intopartyAReservedOpenFees. -
Executed uses
requestedOpenPricefor limit quotes andopenedPricefor 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.
sendQuotedebits the reserved fee from allocated balance and records it as a reservation.-
openPositionwritesopenedPrice, then moves only the difference between reserved and executed on allocated balance. A shortfall is debited asPLATFORM_FEE_OUT; an excess is credited back asPLATFORM_FEE_IN. - 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:
- Cancelling or expiring a pending quote, and force-cancelling a locked one.
- PartyB unlocking a quote it had locked.
- The unfilled remainder of a partially filled limit quote that is cancelled.
- PartyA and PartyB liquidation of pending quotes, including clearing-house takeover and escrow accumulation.
- 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
-
TradingFeeChargedwithTradingFeeType.OPENnow carries the executed fee. Indexers that recomputed the fee frommarketPricewill disagree with the event for market orders and should read the event value. -
onFeeChargedreceives the same executed amount on both the affiliate and the system hook, so affiliate revenue accounting matches what the collector received. -
Opening a market position can now emit a
BalanceChangePartyAevent for PartyA. Consumers that assumedopenPositionnever moved allocated balance for PartyA need to account for the adjustment. - 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.