Start here
A venue sometimes changes how many units a symbol trades in. A 4:1 split turns one unit into four, a 1:10 reverse split turns ten into one, and a rebase moves the count some other way. The position is worth exactly what it was worth a second earlier. What changed is the vocabulary: every quantity and price Core has in storage is now written in the old units, while the venue feed has started speaking the new ones. This page is about never letting those two vocabularies mix.
Core gives Operations two ways to handle that, and most of this page is the detail of each:
- Translate. Leave stored quotes alone and have every incoming venue price multiplied by a stored factor so it lands back in the old units. One transaction, trading resumes immediately, and in exchange every integration must keep translating for as long as that factor is active. This is the adjusted-price route.
- Rewrite. Freeze trading on the symbol and convert the stored quotes themselves into the new units, then resume with no translation anywhere. Correct at rest, at the cost of a real maintenance window. This is the physical route.
Scheduling an adjustment does not commit to either one. Operations picks the route after the venue outcome is known, and can take the adjusted-price route first and the physical route later.
Who does what
| Name | Role on this page |
|---|---|
| Muon | The off-chain oracle and signature service. It supplies prices and UPNL values, and it decides which unit basis those values are expressed in. Core verifies the signature, never the basis. |
| PartyA | The trader. |
| PartyB | The solver or hedger on the other side. It owns its quotes: during a window it can rewrite its own, and it is the only caller at all for emergency-closing them. |
| Symbol manager |
An address holding SYMBOL_MANAGER_ROLE. Every lifecycle call here is manager-only unless noted.
|
| Operations | This page's word for the people and systems coordinating that role, Muon's timing, and the off-chain book and inventory checks. Core enforces none of this coordination. |
The unit-basis problem
A unit-changing venue event changes quantity and price together. A 4:1 split turns one old unit into four new units, while the venue price becomes about one quarter of its old value. The economic position has not changed.
Core does not automatically rewrite an open quote when the venue changes. If a quote still stores one unit opened at
400, comparing it with the venue's new-unit price of 100 produces a false loss. The same mismatch
would affect notional, funding per unit, liquidation prices, and any signature that carries a price or UPNL.
The two routes keep the invariant in opposite ways. On the adjusted-price route, Core leaves quote storage alone and only stores and exposes the factor, so Muon and PartyB integrations carry the burden of converting external prices and funding inputs back to the stored quote basis. On the physical route, Core freezes ordinary execution and converts eligible quote storage to the venue's new basis. Liquidation is the deliberate exception: it remains available during the window and converts each venue-basis liquidation price to that quote's current stored basis before closing it.
The mechanism covers splits, reverse splits, and quantity rebases. It does not model cash dividends, spin-offs, mergers, ticker migrations, or normal price moves.
Basis and factor vocabulary
Two ways to say “one unit”
The venue feed reports a price in the units currently traded by the venue. This page calls that the raw venue basis. A quote's fields use the units currently held in Core storage. This is the stored quote basis. They are the same before an adjustment and after a complete physical restatement. They differ while an adjustment factor is active.
Three factors, three questions
A factor is one venue event written as new units per old unit, scaled by 1e18. A 4:1 split is
4e18; a 1:10 reverse split is 0.1e18. Core stores the latest step it was told about, and the range
it accepts is [1e16, 100e18] excluding 1e18 itself.
Three different numbers are built from those steps, and they are easy to confuse because in the simple case they are all the same. They differ only in which events they include, and each one answers its own question:
| The question | The number | What it includes |
|---|---|---|
| What must external prices multiply by right now? | Active factor — getCumulativeFactor |
Every confirmed event that quote storage has not absorbed. Raw zero in storage means unset, and this view
reports it as 1e18.
|
| What would they multiply by once the pending step is confirmed? | Prospective factor — getProspectiveCumulativeFactor |
The active factor compounded with the SCHEDULED step, while one is in flight. With no step in
flight it equals the active factor.
|
| What is this restatement window converting storage by? | restatementFactor |
Fixed when startRestatement runs. A direct restatement selects the prospective factor; a
restatement opened later selects the active factor.
|
A worked sequence makes the difference concrete. A 2:1 split has already been confirmed, and a 3:1 split has just been scheduled:
active factor = 2e18 external prices multiply by 2 today
scheduled step = 3e18 known, but not yet in effect for anyone
prospective factor = 6e18 floor(2e18 * 3e18 / 1e18)
then, depending on what Operations does next:
confirmPriceAdjusted -> 6e18 becomes the active factor; storage untouched
startRestatement -> restatementFactor = 6e18; the active factor stays 2e18
cancelAdjustment -> the 3:1 step is dropped; the active factor stays 2e18
Note the third line of that outcome list: cancelling a step never reaches back and undoes an earlier confirmed one. An active factor survives scheduling, cancellation, and everything else until a physical restatement absorbs it into storage.
How factors compound
prospectiveFactor = floor(activeFactor * scheduledFactor / 1e18)
Flooring is deliberate and it is the source of most of the edge cases further down the page: a confirmed
2e18 factor followed by a scheduled 3e18 step gives exactly 6e18, but repeated reverse
splits grind the product downward one truncation at a time.
Freeze is computed, not stored
There is no FROZEN lifecycle state and no keeper transaction that sets one. Core evaluates a predicate on every
affected call:
frozen = restating || (state == SCHEDULED && block.timestamp >= effectiveTimestamp)
So a symbol freezes by itself the moment its effective time arrives, with nobody sending a transaction, and it unfreezes only
when something makes the predicate false: confirmation, cancellation, or finalizing a restatement window. Note that aborting a
window is not on that list — an abort clears restating, but on the direct route the state is still an
effective SCHEDULED, so the symbol stays frozen. isSymbolFrozen(symbolId) evaluates the predicate
live. A freeze is also not a symbol-wide pause — it blocks a specific list of paths, and
several quote mutations stay callable throughout.
One more term before the routes. Physical restatement is the name of the maintenance window itself: eligible stored
quotes are rewritten to the raw venue basis, funding is crystallized at one cutoff and restarted in a fresh epoch on exit, and
basisVersion advances when the window finalizes.
One split, two route outcomes
Use one 4:1 split to compare the two routes. Before the event, an open long has one unit opened at 400. Its
stored current funding rate is 8 per old unit. At the event, the venue changes to four units priced at
100 each.
Before the event Raw venue basis after the event
quantity = 1 quantity equivalent = 4
openedPrice = 400 raw venue price = 100
notional = 1 * 400 = 400 notional = 4 * 100 = 400
funding amount = 1 * 8 = 8
If Core used the raw price 100 with the old stored quantity and opened price, the long's price UPNL would be
1 * (100 - 400) = -300. The correct UPNL at the unchanged economic price is zero.
On the adjusted-price route, Muon multiplies the raw venue price by the active factor. Core sees an old-unit mark of
100 * 4 = 400, matching the unchanged quote. Quantity, notional, UPNL, and funding all remain on the old basis.
On the physical route, Core rewrites the quote to four units opened at 100. The current funding rate is restored
as 8 / 4 = 2 per new unit. The products remain 4 * 100 = 400 notional and
4 * 2 = 8 funding. After finalization, Muon can use the raw 100 price without conversion.
What each route leaves you with
| Adjusted-price route | Physical route | |
|---|---|---|
| Stored quote fields | Untouched. Still on the old unit basis. | Rewritten in batches to the venue's new unit basis. |
| What Muon publishes | Raw venue price multiplied by the factor, for as long as that factor stays active. | Raw venue prices again, once the window has finalized. |
PartyB marketPrices |
Old-unit adjusted. Core neither converts nor validates this input. | Raw venue units after finalization. |
| Funding rates | Untouched, still per old unit. |
Crystallized exactly at the start cutoff, paused without adding zero-rate history, then restarted in a fresh
epoch at finalization with the current rates divided by restatementFactor.
|
| Trading | Resumes the moment confirmPriceAdjusted lands. |
Suspended for the whole window, which spans several transactions and phases. |
| Cost to run | One manager transaction, after off-chain verification that the feed basis really did change. | A multi-phase operation: PartyB preparation, dust cleanup, rewrite batches, a signature-expiry wait, finalization, and funding restoration. |
| What you owe afterwards | Every integration must keep translating until a restatement absorbs the factor. | Nothing. basisVersion advances and conversion disappears from the system. |
The panel below runs the same arithmetic Core does, on numbers you choose. It is the quickest way to check a specific quote against the conversion rules described later in Conversion arithmetic and fields.
What happens after scheduling
A manager schedules a step with scheduleAdjustment(symbolId, factor, effectiveTimestamp). The symbol stays live
before a future effective time. It freezes automatically at equality, without a keeper transaction. Core accepts a past
effective time, so an emergency schedule can freeze in the scheduling transaction. Core records that transaction time
separately in adjustmentScheduledAt.
The factor must be between 1e16 and 100e18, inclusive, and cannot equal 1e18. Only one
SCHEDULED step may be in flight, and no schedule may be added during a restatement window. A new step can be
scheduled after PRICE_ADJUSTED, APPLIED, or CANCELLED. Existing active factors survive
the new schedule.
Scheduling does not choose a route. Once the venue outcome is known, Operations either cancels a called-off event or handles an event that happened through one of two routes.
If the venue event is called off
Call cancelAdjustment. This is an exit from the lifecycle, not a third adjustment route, because there is no unit
change to handle.
Cancel the scheduled adjustment
Cancellation is valid before or after the effective time if no restatement window is open. No new conversion or quote restatement is needed for the called-off event. Any older active factor remains in use.
-
Register
scheduleAdjustmentSCHEDULEDLive until effective -
Exit
cancelAdjustmentCANCELLEDLive
If the venue event happens, choose one of two routes
The adjusted-price route keeps stored quotes on the old unit basis and converts external prices to match them. The physical
route rewrites stored quotes to the venue's new unit basis. abortRestatement is not another route. It only backs
out while no open quote has been rewritten.
Keep stored quotes and convert external prices
Confirm that Muon is publishing prices on the stored quote basis. Physical restatement can happen later.
-
Register
scheduleAdjustmentSCHEDULEDLive until effective -
AutomaticeffectiveTimestamp
SCHEDULEDFrozen -
Resume
confirmPriceAdjustedPRICE_ADJUSTEDLiveMuon and funding inputs remain on the adjusted old-unit basis.
Optional later conversion
Freeze again when Operations is ready to rewrite stored quotes.
-
Open window
startRestatementPRICE_ADJUSTED + restatingFrozenSeal the PartyB manifest first; abort then remains available until a basis-dependent mutation.
-
Prepare funding
processRestatementFundingFUNDING_PREPARATIONFrozenSubmit PartyB address batches, then attest that preparation is complete.
-
Settle funding
applyAdjustmentFUNDING_SETTLEMENTFrozen -
Rewrite batches
applyAdjustmentQUOTE_PROCESSINGFrozen -
Begin commit
finalizeRestatementFINALIZATION_FUNDING_RESTORATIONFrozenIf no funding pair was checkpointed, this call completes the window immediately.
-
Finish funding
processRestatementFundingAPPLIEDLive after the last batch
Rewrite stored quotes to the venue basis
Skip the adjusted-price trading phase and move stored inventory directly to the venue basis.
-
Register
scheduleAdjustmentSCHEDULEDLive until effective -
AutomaticeffectiveTimestamp
SCHEDULEDFrozen -
Open window
startRestatementSCHEDULED + restatingFrozenThe opening call records the window and cutoff. Prepare and seal the PartyB manifest before abort or quote processing; after sealing, abort remains available until the first basis-dependent mutation.
-
Prepare funding
processRestatementFundingFUNDING_PREPARATIONFrozenSubmit PartyB address batches, then attest that preparation is complete.
-
Settle funding
applyAdjustmentFUNDING_SETTLEMENTFrozenSettle every prepared open quote before rewriting any quote.
-
Rewrite batches
applyAdjustmentQUOTE_PROCESSINGFrozenOptionally cancel pending inventory and rewrite eligible open quotes.
-
Begin commit
finalizeRestatementFINALIZATION_FUNDING_RESTORATIONFrozenIf no funding pair was checkpointed, this call completes the window immediately.
-
Finish funding
processRestatementFundingAPPLIEDLive after the last batch
Cancel and abort do different jobs
| Action | When it is available | Result |
|---|---|---|
cancelAdjustment |
The state is SCHEDULED and no restatement window is open. |
Marks the latest schedule CANCELLED. The symbol stays live or becomes live again. Any older
active factor remains in use.
|
abortRestatement |
The PartyB manifest is sealed and no basis-dependent mutation has occurred. |
Starts a fresh original-rate epoch at the abort timestamp. If checkpoints exist, Operations supplies their
PartyB addresses to
processRestatementFunding; the last batch closes the window and emits
RestatementAborted.
|
After a direct-route abort, the state is still effective SCHEDULED, so the symbol remains frozen. Operations must
restart restatement, switch to the adjusted-price route with confirmPriceAdjusted, or cancel if the event was
called off. An abort from a later window normally returns to live PRICE_ADJUSTED. Core rejects abort once a quote
rewrite or a PartyA liquidation price snapshot sets restatementMutated. Once abort restoration starts, Core
rejects new basis-dependent mutations and rechecks the flag before completing the abort. Funding settlement, atomic
liquidation removal, and pending-quote cancellation do not set this flag because they do not leave basis-dependent state
behind. Operations must still reconcile the book before aborting. An aborted window's epoch is not reused.
Abort completion sets restating = false, changing the signed price-basis commitment. With funding checkpoints,
this happens in the final restoration batch. Signatures from the open window then fail verification, including future-dated
payloads. Muon can issue replacement prices in the restored basis using an existing liquidation's original timestamp and ID.
There is no abort-completion timestamp cutoff; replay protection comes from the signed epoch and window state.
Adjusted-price route without quote rewrites
This route resumes trading without rewriting quotes. Muon first changes the external price basis while quote storage stays unchanged. At or after the effective time and before confirmation, the feed factor is the prospective factor. After confirmation, it is the active factor:
adjusted old-unit price = floor(raw venue price * feed factor / 1e18)
venue quantity = floor(stored quantity * feed factor / 1e18)
venue closed amount = floor(stored closed amount * feed factor / 1e18)
venue open amount = venue quantity - venue closed amount
The getQuoteInVenueUnits view, and its batch form, return normalized copies by scaling stored total and closed
amounts separately. Their difference can be one amount wei away from
floor(stored open amount * feed factor / 1e18). This view conversion does not modify quote storage. Normal Core
execution still expects the stored old-unit values on this route.
- At the effective time, confirm that
isSymbolFrozen(symbolId)is true. - Muon reads
getProspectiveCumulativeFactorand starts producing old-unit adjusted prices. - Operations verifies the feed basis independently.
- Operations calls
confirmPriceAdjusted.
Confirmation changes state to PRICE_ADJUSTED, writes the prospective value to cumulativeFactor, and
removes the scheduled freeze. Stored quotes do not change. Muon and PartyBs must continue using old-unit adjusted prices while
that factor is active. This includes the marketPrices input used for funding updates. Core does not apply the
factor to that input or validate its basis.
The active factor may remain in use. If Operations later chooses physical restatement, it opens a new frozen window with
startRestatement. The window selects the active cumulative factor, not merely the latest scheduled step.
Physical route with quote restatement
There are two moments a physical restatement can begin, and they differ only in which factor the window locks in:
-
Directly, from an effective
SCHEDULEDstep. The window selects the prospective factor, but does not activate it incumulativeFactor— storage absorbs it instead. - Later, while a non-1x active factor exists. The window selects that active factor.
Either way, startRestatement first checks the caller's expected global liquidation-start nonce. If it matches,
the function does the same small amount of work: it increments restatementEpoch, sets restating,
saves restatementFactor, and records one funding cutoff. It is worth being explicit about what it does
not
do, because this is a common wrong assumption: it never scans the PartyB registry and never snapshots positions or liquidation
flags on its own.
It always opens FUNDING_PREPARATION instead, and waits. Operations supplies every involved PartyB in bounded
batches so Core can snapshot open-position totals and, when accumulated funding is active, checkpoint funding rates.
Operations then seals the submitted manifest, and only then does quote processing become available.
startRestatement can also be called from CANCELLED when an older non-1x factor is still active. For
example, Operations may confirm one adjustment, schedule a newer adjustment, and then cancel only the newer step. Cancellation
changes the state to CANCELLED but does not clear the earlier cumulativeFactor, so
startRestatement can still open a physical-restatement window using that older factor.
Operations sequence for physical restatement
The paragraphs above explain when physical restatement can start and which factor it uses. Once Operations chooses this route, it follows the sequence below. Some steps are off-chain checks; the named functions are on-chain transactions.
-
Find quotes that cannot survive the conversion. Operations runs
previewQuoteAdjustmentagainst every eligible open quote. When amount rounding makes a quote unrestatable, Operations marks it for PartyB cleanup once the symbol is frozen. -
Open the restatement window. From one consistent chain snapshot, Operations verifies that no affected
liquidation is active and reads
getLiquidationStartNonce(). It passes that value tostartRestatement(symbolId, expectedLiquidationStartNonce). Core rejects a changed nonce, otherwise freezes the symbol, records the factor and funding cutoff, and emitsRestatementStarted. This transaction does not iterate over registered PartyBs. -
Prepare every involved PartyB. Operations builds the complete PartyB set off-chain from its open book and
funding records. The bot simulates and submits
processRestatementFunding(symbolId, partyBs)in gas-bounded address batches. Core snapshots LONG and SHORT totals for only those addresses and checkpoints funding when active. After reconciling the complete set, Operations callscompleteRestatementFundingPreparationto seal it. When accumulated funding is active and open inventory exists, Core entersFUNDING_SETTLEMENT; otherwise it entersQUOTE_PROCESSING. -
Close every marked quote. Muon supplies a stored-basis close signature, the Operations bot simulates the
exact transaction, and that quote's PartyB calls
emergencyClosePosition. The dust exception does not require PartyA to act. -
Establish the old-basis signature cutoff. After the cleanup signatures are issued, Muon observes
RestatementStartedor reads the open restatement state and stops issuing affected old-basis signatures. Operations records the latest possible timestamp among the cleanup signatures and every other affected payload. -
Check the frozen book. Before changing any quote, Operations reads every pending and open quote for the
symbol. If amount dust appeared after the earlier scan, repeat the PartyB cleanup and establish a new cutoff that includes
the later close signature. If any other quote cannot be cancelled, closed, or rewritten, Operations calls
abortRestatementwhile abort is still available. If funding checkpoints exist, the bot then callsprocessRestatementFundingwith the checkpointed PartyB addresses untilRestatementAbortedis emitted. -
Settle funding, then simulate every rewrite batch. During
FUNDING_SETTLEMENT, Operations callsapplyAdjustment(symbolId, quoteIds)in batches for every prepared open quote. This pass settles old-basis funding without rewriting quotes, and Core advances only after its settlement inventory reaches zero. Operations then useseth_callto simulate each exact rewrite batch. This runs authorization, quote-status, epoch, liquidation, conversion, and aggregate-accounting checks without saving any changes. If a batch reverts, Operations resolves the cause or aborts before the first rewrite. Simulation does not reserve state, so the bot repeats it immediately before submitting the batch and whenever the caller, IDs, or chain state changes. -
Convert the open book. Operations calls
applyAdjustmentin batches for eligible open quotes. It may callcancelPendingQuotesto release pending balances before finalization, but pending cleanup is not a finalization prerequisite. - Verify open-position coverage. Operations reads the open book again and confirms that every affected open quote was closed or rewritten. Pending quotes that remain will be invalidated by the final quote-ID cutoff.
-
Finalize, then restore funding in batches. After the last possible old-basis Muon signature has expired
and the contract's time gate has passed, Operations calls
finalizeRestatement. If checkpoints exist, this beginsFINALIZATION_FUNDING_RESTORATION; it does not yet unfreeze the symbol. The bot simulates and submitsprocessRestatementFundingwith the checkpointed PartyB addresses untilRestatementFinalizedis emitted. Only that terminal batch advancesbasisVersion, records the pending-quote ID cutoff, and unfreezes the symbol. With no checkpoints,finalizeRestatementcompletes those terminal changes itself.
Liquidation-start race protection
MAStorage.liquidationStartNonce is a global monotonic sequence. It increments exactly once whenever a successful
PartyA liquidation starts through the legacy, deferred, or snapshot route; whenever an isolated PartyB liquidation starts,
including escalation from force close; and whenever Clearing House starts cross-PartyB liquidation. A snapshot single-step
PartyA liquidation also increments it even when that transaction finishes the liquidation. Failed starts do not increment it.
PartyA takeover does not increment it because takeover continues an already-started liquidation.
snapshot block: no affected liquidation; liquidationStartNonce = 82
liquidation mined before restatement -> nonce becomes 83 -> startRestatement(symbolId, 82) reverts
restatement mined before liquidation -> nonce is still 82 -> restatement starts; later liquidation uses the restatement-aware path
The nonce is global because several liquidation-start payloads identify an account or PartyA/PartyB pair without listing every symbol exposed by its open positions. A per-symbol increment would require an unbounded position scan or another on-chain exposure index. The global design adds constant work to liquidation start and restatement start, at the cost of a harmless stale-nonce revert when an unrelated-symbol liquidation starts. Operations then refreshes its checks and retries with the new value.
finalizeRestatement records one funding-resumption timestamp. That timestamp starts the new-basis funding epoch
for every checkpoint, even though Operations materializes the stored records in later batches. The pre-window cumulative fee
remains in each snapshot; the maintenance interval is never averaged into it. Every restoration batch uses the same timestamp.
The terminal transaction writes state APPLIED only when the prior state was SCHEDULED or
PRICE_ADJUSTED, sets cumulativeFactor to an explicit 1e18, closes the window, clears
the temporary funding fields, increments basisVersion, and stores the current global quote ID as
SymbolAdjustment.pendingQuoteIdCutoff. It retains the latest per-step factor and
effectiveTimestamp.
What physical restatement changes
Eligibility, authorization, and batch behavior
applyAdjustment(symbolId, quoteIds) requires an open window. A manager may process any quote. A PartyB may
process only its own quote, and authorization is checked separately for every ID. Each quote must match the symbol, have an
epoch older than the current window, and have status OPENED, CLOSE_PENDING, or
CANCEL_CLOSE_PENDING. The call reverts if PartyA is being liquidated, or if PartyB is in isolated liquidation
against that PartyA or in cross liquidation.
Each call is atomic. During FUNDING_SETTLEMENT, any invalid ID or failed funding charge rolls back the batch.
During QUOTE_PROCESSING, any invalid ID, arithmetic failure, aggregate overflow, or downstream revert rolls back
every earlier quote in the batch. A duplicate epoch reverts with Already restated. Repeating a completed ID
reverts, so retries must omit completed IDs. An empty array is accepted and changes nothing.
Conversion arithmetic and fields
previewQuoteAdjustment(symbolId, quoteId) is a read-only preflight helper. It selects the applicable adjustment
factor and returns the converted quote fields that applyAdjustment would write. It uses the same arithmetic and
underflow checks as the write path, but it does not check quote status, liquidation state, authorization, or whether funding
settlement will succeed.
Core first separates the economically live and historical pieces of a position. It scales the actual open amount (quantity - closedAmount) and closedAmount independently, then reconstructs quantity as their exact sum. It derives each
new price from the corresponding old notional. This keeps position size conserved across the open/closed boundary while
keeping amount * price as close as integer rounding permits.
oldOpenAmount = quantity - closedAmount
adjustedOpenAmount = floor(oldOpenAmount * factor / 1e18)
adjustedClosedAmount = floor(closedAmount * factor / 1e18)
adjustedQuantity = adjustedOpenAmount + adjustedClosedAmount
adjustedAmount = floor(originalAmount * factor / 1e18)
if originalPrice == 0:
adjustedPrice = 0
else:
adjustedPrice = floor(originalAmount * originalPrice / adjustedAmount)
Factors and all amount and price fields use 18 decimals. Math.mulDiv performs each calculation and rounds down.
| Part of the quote | What Core changes |
|---|---|
| Entire position |
Reconstructs quantity from the separately scaled open and closed components. Recalculates
openedPrice, initialOpenedPrice, requestedOpenPrice, and
marketPrice against the new quantity so each recorded amount-and-price value stays as close as
possible to its previous notional.
|
| Already-closed portion |
When closedAmount is nonzero, scales it and recalculates avgClosedPrice to preserve
the recorded closed notional as closely as rounding permits.
|
| Outstanding close request |
When quantityToClose is nonzero, scales it and recalculates requestedClosePrice to
preserve the requested close notional as closely as rounding permits.
|
The conversion assigns exactly those nine fields. It does not assign IDs, parties, symbol, position or order type, whitelists,
statuses, deadlines, parent links, locked values, fee fields, affiliate, funding limit, close fee, or data. When accumulated
funding is active, the earlier FUNDING_SETTLEMENT pass sets lastFundingPaymentTimestamp and may
change accumulatedPaidFunding and party balances. The later QUOTE_PROCESSING pass performs only the
unit conversion and related aggregate updates.
A reverse split can make a very small raw amount disappear when multiplication is rounded down, and Core would rather refuse
the conversion than silently erase a field. Four checks guard that. The examples below all use a 0.01x factor,
and the numbers are raw 18-decimal amount integers, so one unit here means one amount wei.
| What would disappear | Example at 0.01x |
The rule Core enforces |
|---|---|---|
| The entire position | quantity = 99 → floor(99 * 0.01) = 0 |
The converted quantity must be greater than zero. Core will not store a zero-quantity position. |
| Recorded closed quantity | closedAmount = 1 → floor(1 * 0.01) = 0 |
A nonzero closedAmount must stay nonzero, so the closed history is not erased. |
| The remaining open quantity |
quantity = 1e18 + 2 and closedAmount = 1e18 + 1, one amount wei still open; at
0.5x, that actual open amount converts to 0
|
The directly converted actual open amount must stay greater than zero. |
| A pending close request | quantityToClose = 1 → 0 |
A nonzero quantityToClose must stay nonzero. Core will not keep a close request for zero
quantity.
|
A quote that trips any of these is unrestatable. It cannot be rewritten, so it has to leave the book another way, which is what the dust exception below is for. The calculator above reports which of the four rules a given quote trips.
Price rounding is different: it does not reject every remainder. Core divides the old notional by the adjusted amount and rounds the price down. The discarded remainder is the notional dust. For example:
old notional = 3 * 10 = 30
adjusted amount = 4
adjusted price = floor(30 / 4) = 7
new notional = 4 * 7 = 28
dust = 30 - 28 = 2
The dust is simply the remainder from integer division: oldNotional % adjustedAmount. It is always zero or
positive and always smaller than adjustedAmount.
Funding settlement and aggregate updates
For each quote rewrite, the facet executes this order:
- Require that Operations prepared the quote's PartyB inventory.
- If accumulated funding was active when preparation was sealed, require the earlier funding-only pass to have settled the quote.
- Remove old open amount and opened-price notional from PartyB global, PartyB per-PartyA, and PartyA per-PartyB position aggregates. The shared helper also subtracts the old weighted-funding contribution and consumes the quote's old-basis restatement inventory.
- Rewrite the nine unit-bearing quote fields.
- Add the new position aggregates and one new weighted-funding contribution.
- Stamp
quoteRestatedEpoch, set the mutation flag, and emitQuoteAdjusted.
The subtraction occurs once in LibQuote.subFromPartiesAggregatedPositions, and the new contribution is added once
after mutation. The same shared subtraction is used by liquidation and every other full-close path, so a liquidation close
cannot leave either position aggregates, weighted aggregate funding, or restatement inventory behind.
Liquidation while a restatement is open
An open physical-restatement window is a narrow exception to the ordinary symbol freeze for liquidation only. Legacy and
snapshot PartyA price setup, isolated PartyB position liquidation, and Clearing House position liquidation may resolve the
affected symbol. An effective SCHEDULED adjustment with no open window remains frozen and these paths still
reject it. Existing liquidation roles, account state, pause, quote ownership, signature, timeout, settlement, and hook checks
are not weakened.
During the open window, every liquidation price supplied for the affected symbol is in normalized venue units. Core converts it separately for each quote before PnL calculation, average-close accounting, hooks, and trade-volume events:
if quote was restated in the current epoch:
stored liquidation price = venue liquidation price
else:
adjusted open amount = floor((stored total quantity - stored closed amount) * restatementFactor / 1e18)
adjusted closed amount = floor(stored closed amount * restatementFactor / 1e18)
adjusted total quantity = adjusted open amount + adjusted closed amount
stored liquidation price = floor(adjusted total quantity * venue liquidation price / stored total quantity)
The quote-specific reconstructed-quantity ratio is the inverse of the physical price rewrite and therefore follows the same
conservation-preserving integer rounding. It lets one PartyA symbol price safely close a mixed batch containing both rewritten
and old-basis quotes. If an unrestatable dust quote's adjusted total quantity is zero, Core uses
floor(venuePrice * restatementFactor / 1e18) as the deterministic stored-price fallback; Operations should still
prefer the dedicated PartyB dust-close flow because no exact normalized quantity exists for that quote.
Muon liquidation price hash format
The liquidation signature structs and function selectors stay the same, but the signed hash changes. Muon and its gateway must
both sign the following envelope. payloadHash is the previous hash from verifyLiquidationSig,
verifyDeferredLiquidationSig, verifyLiquidationSnapshotSig, or verifyQuotePrices,
including its existing chain ID.
domain = keccak256("SYMMIO_LIQUIDATION_PRICE_BASIS_V1");
priceBases[i] = keccak256(abi.encode(
symbolIds[i], // uint256
restatementEpoch[i], // uint256
restating[i] // bool
));
signedHash = keccak256(abi.encode(domain, payloadHash, priceBases)); // bytes32, bytes32, bytes32[]
Use the supplied symbol IDs for legacy/deferred PartyA payloads, each state's symbol ID for snapshots, and each quote's
on-chain symbol ID for PartyB quote prices. Preserve the exact payload order and duplicate entries. Empty snapshot-start
payloads still use the envelope with an empty bytes32[]; they authorize no prices. Read each symbol's epoch and
flag through getSymbolAdjustment at the same block used to construct its prices and funding values.
Opening a window increments its epoch and sets restating = true. The flag stays true throughout
abort/finalization restoration and changes to false only at completion. A later window increments the epoch again. These
transitions invalidate the previous price signatures permanently, regardless of their timestamps. Changes to unrelated symbols
do not invalidate a payload. Release the Muon/gateway hash update together with these contract changes: signatures over the
previous hash are no longer accepted, including for symbols that have never been restated. There is no fallback to the
previous hash.
Liquidation funding follows the same cutoff as ordinary quote debt. Before an exit begins, internal quote and aggregate calculations use the crystallized cutoff value even if Operations has not yet materialized that PartyB's checkpoint; the later preparation batch writes exactly that value. A liquidation close may realize the unpaid pre-cutoff amount and consume the old-basis funding inventory, but the elapsed maintenance time contributes nothing. Once abort or finalization starts restoration, internal calculations use the fresh epoch from the shared restoration timestamp, including before that PartyB's restoration batch lands. The snapshot PartyA route carries Muon-signed cumulative fees instead of recomputing them at close time, so Muon must publish the frozen cutoff cumulative value during maintenance and the fresh-epoch cumulative value after an exit begins. The cumulative funding fields retain their existing types and follow this lifecycle; the signature hash uses the price-basis envelope above.
When an old-basis quote closes, closePositionFully calls the inventory hook before subtracting normal aggregates.
If the PartyB was already prepared, the hook subtracts the quote's exact open amount from the PartyB checkpoint and symbol
total and emits RestatementInventoryConsumed. During FUNDING_SETTLEMENT, it also subtracts any
still-unsettled funding inventory; the close that clears both funding totals emits
RestatementFundingSettlementCompleted and advances to QUOTE_PROCESSING. A close before that PartyB
is prepared needs no counter write because the later live aggregate snapshot observes the already-removed position. After
preparation is sealed, an omitted PartyB cannot mutate old-basis inventory: the call reverts with
PartyB inventory not prepared. This prevents liquidation from bypassing a sealed checkpoint, but it does not
prove that the submitted PartyB manifest was complete; finalization still trusts the manager's seal and Operations must
reconcile every open position off-chain.
An atomic isolated-PartyB or Clearing House close does not set restatementMutated: it removes a position instead
of rewriting its basis, so a window containing only those closes can still be aborted safely. PartyA liquidation is
multi-step. When its price-setting step stores a venue-basis price for a restating symbol, Core sets
restatementMutated immediately. This prevents an abort from returning to the old basis and then reinterpreting
that stored price. During ABORT_FUNDING_RESTORATION, legacy, deferred, and snapshot PartyA price setup for the
affected symbol reverts, including between funding-restoration batches. After completion, the price-basis commitment rejects
signatures from the aborted window, even if they already started a liquidation or have a future timestamp. A snapshot
liquidation can resume with replacement prices signed for the restored basis while preserving its original timestamp,
liquidation ID, and historical fields. Abort does not require Clearing House takeover. Atomic isolated-PartyB and Clearing
House closes remain available under their existing guards. There is no governance function that zeros inventory or forces
finalization past nonzero counters. The emergency governance route is the existing CLEARING_HOUSE_ROLE takeover
of an active PartyA liquidation, or cross-PartyB liquidation, followed by explicit quote-ID batches and normal settlement. It
cannot fabricate completion and it does not bypass financial accounting.
Pending inventory
Physical finalization records the current global quote ID in SymbolAdjustment.pendingQuoteIdCutoff. A
PENDING, LOCKED, or CANCEL_PENDING quote for that symbol is stale when its ID is at or
below the cutoff. Stale quotes cannot be locked or opened. A quote created after finalization receives a larger ID and is
valid even when both transactions share the same block timestamp. Locking or opening a stale quote reverts with
PendingQuoteIsStale(). Price-only confirmation and an aborted restatement do not advance the cutoff.
cancelPendingQuotes remains an optional manager-only cleanup while the symbol is frozen. It accepts the same
three pending states without checking their deadlines. After finalization, anyone may call
cancelStalePendingQuotes for quotes proven stale by the cutoff. Both paths refund the open trading fee, release
pending locks and PartyB connections where applicable, remove pending indexes, set the quote to CANCELED, and
call cancellation hooks. Liquidation guards still apply, a hook revert rolls back the batch, and an empty array changes
nothing.
A stale quote is economically inert but remains in pending indexes and keeps balances reserved until it is cancelled, expired,
or removed by liquidation. Applications should use isPendingQuoteStale to offer lazy cancellation. Removing
pending inventory does not set the restatement mutation flag because it never entered the open book.
Mixed-book reads
Raw storage is mixed while batches run. getQuote returns that storage unchanged.
getQuoteInVenueUnits and getQuotesInVenueUnits return converted memory copies that can support
display, valuation, reconciliation, and hedging when the selected factor represents the intended venue basis. They must not be
sent back into Core execution.
| Quote condition | factorApplied in venue-unit view |
|---|---|
| Restated in the current open epoch | 1e18 |
| Not yet restated in the current open epoch | restatementFactor |
| No open window | Active cumulative factor |
The views do not filter by quote status. When the selected factor is not 1e18, they use the same conversion and
underflow checks as preview. A fully closed quote reverts because its converted quantity is not greater than its converted
closed amount; amount-rounding underflows can also revert. The array getter is atomic, so one non-convertible ID reverts the
entire batch. Use bounded batches with a single-ID fallback, and use raw getQuote for closed history or any
record that cannot be converted.
The venue-unit view also returns the current epoch, freeze status, and storedInVenueUnits. That last flag means
only that this view applied 1e18. It is not independent proof of a quote's provenance or coverage.
Funding across the window
A physical restatement changes quote quantities, and current funding rates are per-unit values. The same rate therefore cannot be used with old-unit and new-unit quantities. Core therefore treats the window as a hard boundary between two funding epochs: it crystallizes the old epoch at the start cutoff, accrues nothing during maintenance, and starts a new epoch on exit.
Operations first supplies every involved PartyB. For each one, Core snapshots that PartyB's total LONG and SHORT open quantities and, when an accumulated-funding record exists, materializes the exact cumulative fee at the shared cutoff. It saves both the original current rates and their restated-basis values, then resets the live epoch counters to a zero-rate paused state. Preparation is sealed before quote processing. On finalization, the restated rates start a fresh epoch; on abort, the original rates start a fresh epoch. Neither exit charges the maintenance pause.
open window → fix cutoff → crystallize old funding → zero-rate pause → settle/restate → fresh funding epoch → close window
When the window opens
startRestatement records one fundingCutoffTimestamp. It does not read or loop over
MAStorage.partyBList, and it does not synchronously write every PartyB's record. From that transaction onward,
quote funding debt and aggregate funding views cap all affected pairs at the cutoff, including pairs that have not yet
appeared in an Operations batch. This makes the economic stop immediate and independent of batch order. Core always enters
FUNDING_PREPARATION so Operations can submit the complete involved-PartyB manifest. When accumulated funding is
inactive, funding checkpoint creation is skipped, but the open-position inventory snapshots are still required.
The raw getFundingFeesOfPartyB record may still show its old current rate before that PartyB is prepared. This is
a storage-materialization detail, not live accrual: quote-debt, settlement, liquidation, and aggregate-debt paths use the
restatement-aware cutoff. Indexers and Operations must distinguish that raw record from the effective economic state.
Prepare PartyB funding pairs
The manager calls processRestatementFunding(symbolId, partyBs) with a gas-bounded address array. Core examines
only those addresses. For every new address in the epoch, it snapshots the PartyB's current total LONG and SHORT open quantity
for the symbol. Repeated addresses are idempotent. Core does not enumerate the global PartyB registry, so the manager remains
responsible for supplying every involved PartyB before sealing preparation.
For each supplied PartyB, Core does one of the following:
- skips a checkpoint already created for the current restatement epoch;
- skips a symbol pair whose
epochDurationis zero; -
computes the exact LONG and SHORT cumulative per-unit fees at
fundingCutoffTimestamp, writes them into the snapshots, saves the original and precomputed restated current rates, incrementspendingFundingPartyBCount, and resets both accumulated and current rates to zero at the cutoff epoch.
snapshot' = snapshot
+ accumulatedRate * (lastUpdatedEpoch - startEpoch)
+ currentRate * (cutoffEpoch - lastUpdatedEpoch)
paused state: snapshot = snapshot', accumulatedRate = 0, currentRate = 0, startEpoch = lastUpdatedEpoch = cutoffEpoch
Every record with a nonzero epoch duration is checkpointed, even when both current rates are zero. That rule matters when the record contains earlier accumulated history: otherwise a later rate update could average the long zero-rate maintenance interval into that history. Repeated addresses are harmless. Current registry membership is not checked, so Operations may submit registered or deregistered PartyBs. Registering or deregistering a PartyB during the window does not change funding progress.
After Operations has submitted and checked the intended set, the manager calls
completeRestatementFundingPreparation. Core rejects later PartyB preparation and enters
FUNDING_SETTLEMENT when accumulated funding is active and prepared inventory is nonzero; otherwise it enters
QUOTE_PROCESSING. The seal is an attestation that the submitted address set is complete. Within that set,
completeness is enforced on-chain: funding settlement and finalization each require their corresponding snapshotted LONG and
SHORT quantity totals to reach zero.
While quotes are being rewritten
A checkpointed pair has zero current rates, zero accumulated rates, and the exact pre-window cumulative value in its snapshots. Funding debt therefore stays constant for an arbitrarily long pause. Before physical checkpointing, the effective read path produces the same result by capping time at the cutoff. Later batches always use that same cutoff even if they run in much later blocks.
During FUNDING_SETTLEMENT, each open quote realizes only its unpaid funding through the cutoff. The quote's
lastFundingPaymentTimestamp, cumulative paid funding, PartyA and PartyB balances, and all weighted
aggregate-funding ledgers move together once. The later quote rewrite changes basis and aggregate quantities but does not
settle funding again. Each successful rewrite subtracts the quote's old quantity from the matching PartyB LONG or SHORT
inventory checkpoint. Ordinary and emergency full closes use the same accounting hook, so removing an old-basis position also
consumes its checkpoint quantity. A quote belonging to a PartyB that was not prepared before the seal is rejected. Pending
quotes are not part of this open-position completeness counter; physical finalization instead makes every older pending quote
stale through the quote-ID cutoff.
Restore rates before closing the window
abortRestatement and finalizeRestatement stop quote processing and start a restoration phase. They
record one shared fundingRestorationTimestamp and report the pending checkpoint count. Economically, that
timestamp ends the pause and starts every pair's fresh epoch. If the count is zero, the initiating transaction closes the
window immediately. Otherwise, Operations submits the saved PartyB manifest through processRestatementFunding in
gas-bounded batches.
| How the window ends | Historical snapshot and pause | Fresh-epoch rate |
|---|---|---|
| Abort | The crystallized snapshot is unchanged; the entire pause remains excluded. | The saved original LONG and SHORT rates restart unchanged at the abort timestamp. |
| Finalization | The crystallized snapshot is unchanged; no zero-rate epochs are appended or averaged. | Each saved rate divided by restatementFactor starts at the finalization timestamp. |
newRate = sign(oldRate) * floor(abs(oldRate) * 1e18 / restatementFactor)
Only the new current-rate conversion can floor; crystallized history is never divided. Core rejects a converted magnitude that
cannot fit in int256. During batched restoration, quote and aggregate funding paths project every still-unwritten
checkpoint from the shared restoration timestamp. A liquidation between the initiating exit transaction and that PartyB's
restoration batch therefore sees the same cumulative funding as it would after the batch, with no pause charge or batch-order
discontinuity. Each restoration batch ignores unknown or repeated addresses, deletes every matching current-epoch checkpoint,
and decrements pendingFundingPartyBCount. The transaction that reduces the count to zero closes the window and
emits RestatementAborted or RestatementFinalized.
Enforced and operational boundaries
What the freeze enforces
The freeze is not a global symbol pause. Internal requireNotFrozen checks block paths that resolve the affected
symbol. In v0.8.6 those checks cover:
- quote creation, PartyB quote locking, open and close execution, batch wrappers, and solver-fee wrappers;
- legacy and unified settlement;
- force actions, three-step force-close finalization, non-dust emergency PartyB actions, and ADL-style closes;
- funding charge, rate setting, epoch-duration changes, and accumulated-funding paths.
emergencyClosePosition has one narrow freeze exception. If the applicable physical factor would make an
unrestated quote fail an amount-underflow check, the quote's PartyB may fully close it with a stored-basis Muon signature.
Core does not treat a quote already rewritten in the current epoch as dust by applying the factor a second time.
The freeze leaves several quote mutations callable. PartyA can call requestToClosePosition, which changes an open
quote to CLOSE_PENDING and writes the requested close price, close quantity, order type, deadline, and
modification timestamp. PartyA can cancel a close request, and anyone can pass an expired close request to
expireQuote; neither path checks the symbol freeze. The frozen manifest is therefore not static. Operations must
monitor and reconcile close-request creation, cancellation, and expiry throughout batching.
Other non-price cleanup can remain callable. A function that neither receives a symbol nor resolves one from a quote cannot apply the freeze check. The freeze also does not cancel an existing liquidation. Liquidation price setup and position-close paths use a narrower guard: an effective scheduled freeze still blocks them, but an actual open restatement window admits them with the quote-local price conversion and inventory accounting described above. Pending-position liquidation does not use a price and remains callable under its existing account-level guards.
Signature expiry and account-level payloads
Ordinary Muon payloads do not carry basisVersion. Core retains a time gate for their expiry alongside the
separate price-basis commitment used by liquidation price signatures. Finalization requires:
block.timestamp > restatementStartedAt + LibMuon.maxUpnlValidTime()
Signature verification accepts the exact expiry boundary and rejects only after it. Finalization uses the strict greater-than
check, so equality still fails. A zero per-function validity override falls back to the global validity.
maxUpnlValidTime() takes the maximum of the global setting and every enum override, using the live configuration
when finalization runs.
For every route, restatementStartedAt is the block time of startRestatement. Old-basis signatures
may still be issued after a scheduled freeze and before the window opens, so neither effectiveTimestamp nor
adjustmentScheduledAt starts the signature-expiry wait. Liquidation price signatures must also strictly postdate
this actual opening time. The timestamp alone does not prove that off-chain issuance stopped then.
Account-level UPNL payloads do not list every symbol in the account. Core cannot infer that such a payload includes exposure to the frozen symbol, so Muon must stop issuing affected account-level signatures off-chain. The cutoff must cover every affected account-, pair-, and quote-level payload. Record the latest old-basis signature timestamp, or prove an upper bound from the issuance cutoff and allowed signer clock skew. Core checks each signature's timestamp and does not reject a future timestamp.
Liquidation price payloads issued for use inside the window are different: their prices are explicitly venue-basis values, so
they do not extend the old-basis cutoff. Core requires each affected PartyA price/snapshot timestamp and PartyB quote-price
timestamp to be strictly later than restatementStartedAt, and verifies the signed restatement state for every
priced symbol. These checks do not validate the basis of the signed UPNL or inspect symbols absent from an account-level
payload; Muon must still construct the UPNL from normalized venue-unit quotes. The ordinary signature expiry and liquidation
timeout checks continue to apply independently.
The on-chain finalization deadline is sufficient only when the proven latest old-basis timestamp is no later than
restatementStartedAt. Otherwise Operations must wait until the block time is strictly greater than the latest
old-basis timestamp bound plus the maximum applicable validity, even if finalizeRestatement would already
succeed. A delayed cutoff is not detected by successful finalization.
Governance and Operations must also prevent a later validity increase from reviving an old-basis signature. Before applying a new global or per-function validity, require the current block time to be strictly greater than the latest timestamp bound for the affected old-basis signatures plus the proposed applicable validity. The recorded restatement opening time is a safe substitute only when the cutoff proof shows that no old-basis timestamp can be later than that opening.
basisVersion and deferred force close
Finalization increments basisVersion, even if no quote was processed. The three-step force-close flow records
this value at initialization. Its refresh step validates a fresh signature but deliberately does not replace the stored close
price or basis version. Finalization of that force close requires the symbol to be unfrozen and the stored version to match,
so an old-basis workflow must be initialized again. This protection is specific to that three-step flow. It is not a generic
field in Muon signatures or every deferred workflow.
Revert-worthy boundaries
-
Schedule rejects an unknown symbol, a factor outside
[1e16, 100e18], factor1e18, an open window, or anotherSCHEDULEDstep. -
Any call that computes a prospective factor can revert if the quotient exceeds
uint256. Confirm and direct start also reject a call beforeeffectiveTimestampand reject a prospective factor of zero. -
Start first rejects
expectedLiquidationStartNoncewhen it differs from the current global nonce withLiquidationStartNonceMismatch(expected, actual). It also rejects an existing window and normalized factor1e18. - Abort rejects no window or any window whose mutation flag is true.
- Apply rejects authorization, symbol, epoch, status, and liquidation failures, plus funding, arithmetic, or aggregate failures reached downstream.
- The dust emergency-close exception requires an unrestated quote that fails an amount conversion. PartyB ownership, liquidation, pause, status, signature, solvency, settlement, and hook checks still apply.
-
Liquidation rejects an effective scheduled freeze unless a restatement window is actually open. During a window, Muon
price payloads reject timestamps at or before
restatementStartedAt; an old-basis close after a sealed manifest also rejects an omitted PartyB. - Pending cancellation rejects an unfrozen symbol, an ineligible status, liquidation guards, or a reverting hook.
- Finalize rejects no window, missing preparation attestation, and the exact or earlier signature-expiry boundary. A funding batch rejects an empty PartyB array, no open window, an invalid phase, or signed-rate overflow. Unknown, repeated, and already processed addresses are skipped.
Actor responsibilities
| Actor | Responsibility outside Core's guarantees |
|---|---|
| Symbol manager and Operations | Verify the venue event and factor direction, choose the route, control Muon timing, maintain a complete PartyB manifest, choose and simulate safe address batches, restore every checkpointed address, stop before an unsafe mutation, read all liquidation flags and the global liquidation-start nonce from one consistent block, rescan everything after a nonce mismatch, coordinate any liquidation that begins later, enforce expiry, and decide whether finalization is safe. |
| Muon and integrators | After the venue event becomes effective and before confirmation, use the prospective factor. After confirmation, use the active factor. Publish old-unit adjusted prices while a factor is active, stop every affected ordinary account-, pair-, and quote-level payload before the cutoff, issue restatement-liquidation prices in venue units strictly after the window start, derive their UPNLs from normalized quotes, and resume ordinary raw prices only after reconciled finalization. |
| PartyBs |
Hedge in the current basis, submit old-unit adjusted marketPrices while a factor is active, and
call applyAdjustment only for owned quotes unless granted the manager role. Fully close owned
adjustment-dust quotes when Operations requests it, using Muon values in each quote's stored basis.
|
| Indexers | Build one manifest across ordinary, isolated, and cross PartyB account models. Include every open and pending status, track epochs and mutation events, and reconstruct history from events because storage keeps only the latest schedule. |
| Auditors and governance | Review role grants, selector compatibility, validity changes, complete-book evidence, PartyB manifests and batch-size limits, liquidation price-basis integration, and inventory/funding reconciliation before approving physical restatement. Do not add a counter-zeroing or forced-finalization bypass. |
Operations runbook
Prepare and schedule
- Verify the venue notice, symbol ID, factor direction, effective time, and allowed factor range.
-
Read the active factor and compute the intended prospective product before scheduling. Require
0 < floor(activeFactor * intendedFactor / 1e18) <= type(uint256).max. Do not infer the new basis from only the latest step. -
Build the complete open quote manifest across all PartyB account models. Pending inventory may also be indexed for
optional pre-finalization refunds and post-finalization lazy cancellation. For every pair, check PartyA liquidation,
PartyB-against-PartyA liquidation, and PartyB cross-liquidation status off-chain. Resolve every active liquidation before
calling
startRestatement. At the same block used for those checks, read and savegetLiquidationStartNonce(). The expected nonce protects against a later liquidation start but does not scan the book or prove that this initial check was complete. - Build the complete involved-PartyB manifest from the affected open book and Operations' funding records. Fork-rehearse inventory and funding preparation, quote processing, and restoration with the intended address batches. Choose a batch size with comfortable gas headroom and test bot restart from the saved manifest.
- Fork-rehearse funded inventory and verify that each rewrite or liquidation close removes the old weighted-funding contribution once, and that a rewrite adds the new contribution once. Include tiny positive and negative rates, large LONG and SHORT positions, a long maintenance pause, zero-current-rate records with prior history, abort, liquidation, and consecutive restatements. Prove the pause contributes exactly zero and no restoration batch changes the effective cumulative value.
-
For partially closed quotes, test raw 18-decimal values on both sides of a multiplication remainder. Independently compute
floor((quantity - closedAmount) * factor / 1e18)andfloor(closedAmount * factor / 1e18); require every preview, venue-unit view, stored rewrite, and liquidation price conversion to use their sum as quantity. Include the boundary where the real open amount rounds to zero and require rejection, not a one-wei position created by independently scaling total quantity. -
Verify the deployed ABI exposes
startRestatement(uint256,uint256),processRestatementFunding(uint256,address[]),completeRestatementFundingPreparation(uint256),cancelStalePendingQuotes(uint256[]),getPendingQuoteIdCutoff(uint256),isPendingQuoteStale(uint256),getLiquidationStartNonce(),getRestatementFundingProgress(uint256), andgetRestatementInventoryProgress(uint256,address). All Symbol Adjustment read selectors are routed throughViewFacetSymbol;SymbolAdjustmentFacetexposes mutation selectors only. Neither the historical three-argument start selector nor the unprotected one-argument selector is exposed. -
Call
scheduleAdjustment. Record the transaction block time, adjustment index, factor, effective time, prospective factor, and current maximum Muon validity. Confirm thatgetProspectiveCumulativeFactorreturns the intended product without reverting. -
Run
previewQuoteAdjustmentfor every eligible open quote now that the schedule supplies the prospective factor. Treat this as an arithmetic preview, not an eligibility or funding-settlement proof. Mark every amount-underflow failure for PartyB cleanup when the symbol reaches its effective freeze.
Reach the effective boundary
- Refresh the manifest before the effective time. Trading is still live for a future schedule.
- For a future schedule, stop ordinary old-basis account-, pair-, and quote-level issuance before the effective time. For a past-effective emergency, stop ordinary issuance before submitting the schedule transaction. Do not establish the final old-basis cutoff yet: dust cleanup may require dedicated close signatures after the symbol freezes.
- At equality or later, verify
isSymbolFrozenand the raw venue feed basis. -
If the event did not occur, call
cancelAdjustment. Verify the symbol unfreezes and the older active factor remains unchanged. End this runbook here; do not run dust cleanup for a called-off event.
Use the adjusted-price route
- Have Muon compute old-unit prices from
getProspectiveCumulativeFactor. - Compare independent calculations with the feed and running notional and UPNL checks.
- Call
confirmPriceAdjusted. VerifyPRICE_ADJUSTED, the expected active factor, and unfreeze. - Resume Muon signatures and PartyB funding inputs in the adjusted old-unit basis.
Use either physical-restatement route
-
Keep ordinary affected signatures stopped. After the liquidation checks, call
startRestatement(symbolId, expectedLiquidationStartNonce)with the saved nonce and record that nonce, epoch, factor, start time, funding cutoff, phase, pending funding count, and the zeroed symbol-wide LONG and SHORT inventory totals. If the nonce mismatches, refresh the entire liquidation snapshot and manifest before retrying; do not merely substitute the new nonce. Do not expect this call to emit checkpoint events, inspect the PartyB registry, snapshot quantities, or zero any rates. -
While progress reports
FUNDING_PREPARATION, simulate and submitprocessRestatementFunding(symbolId, partyBs)from the manager address for each bounded manifest batch. Verify inventory snapshots with events andgetRestatementInventoryProgress; when funding is active, also verify funding checkpoint creation with events, the pending count, orisRestatementFundingCheckpointed. For every checkpoint, independently calculate the cumulative LONG and SHORT fee atfundingCutoffTimestampand match the stored snapshots; verify current and accumulated rates are zero and the epoch counters restart at the cutoff. Include records whose two current rates are zero. Before and after each batch, confirm quote and aggregate funding debts remain fixed at the cutoff. After the complete involved-PartyB set has been supplied and reconciled, callcompleteRestatementFundingPreparation. The manifest is sealed at that transition. If progress reportsFUNDING_SETTLEMENT, useapplyAdjustmentbatches to settle every prepared open quote before submitting any rewrite. Core advances toQUOTE_PROCESSINGonly when the funding-settlement inventory reaches zero. -
Continuously monitor liquidation flags after the window opens. If a new liquidation affects the symbol, stop overlapping
rewrite batches and have Muon issue liquidation prices in venue units with timestamps strictly greater than
restatementStartedAt. Use normalized mixed-book views for UPNL. Process explicit quote-ID batches through the appropriate PartyA, isolated PartyB, or Clearing House path; record everyRestatementInventoryConsumedevent and remove closed IDs from later funding/rewrite batches. Clearing House inputs use venue prices but have no Muon timestamp. -
Re-scan the now-frozen book before the first mutation. Recheck statuses, every liquidation mode, previews, funding
settlement, hooks, and all deregistered PartyBs with open quotes. For every marked or newly discovered amount-underflow
quote, have PartyB obtain a stored-basis Muon signature. Simulate the exact
emergencyClosePositioncall from the PartyB address, submit it, and verify the quote isCLOSED. PartyA does not participate. - After cleanup, record the latest possible timestamp across every old-basis payload, including the dedicated close signatures. Add allowed signer clock skew and prove that Muon cannot issue a later old-basis timestamp. This is the cutoff used for finalization. Fresh restatement-liquidation price payloads are venue-basis and do not move this old-basis cutoff.
-
If any item cannot be processed, abort before a quote rewrite or PartyA liquidation price snapshot. Direct abort stays
frozen in
SCHEDULED. Active-factor abort preserves the prior state, normally livePRICE_ADJUSTED, but it can also return live inCANCELLED. AfterabortRestatement, submit the checkpointed PartyB manifest throughprocessRestatementFundinguntilRestatementAbortedif progress reportsABORT_FUNDING_RESTORATION. Verify that the original rates accrue only from the shared abort restoration timestamp; the start-to-abort interval must remain absent from cumulative funding. -
Optionally call
cancelPendingQuoteswhile frozen to release balances immediately. Pending quotes are not included in the open-position inventory counter and do not block finalization. Monitor close-request creation, cancellation, and expiry throughout the window, and reconcile each resulting status and price-bearing close field before processing an open quote. -
During
FUNDING_SETTLEMENT, submit boundedapplyAdjustment(symbolId, quoteIds)batches and trackQuoteFundingSettledandgetRestatementFundingSettlementProgressuntilRestatementFundingSettlementCompletedreports the move toQUOTE_PROCESSING. Then have the Operations bot simulate each exact rewrite call througheth_callfrom the intended transaction sender. Submit only a successful simulation, trackQuoteAdjusted, and verify each quote's epoch. Remove successful IDs before retrying. A liquidation close can consume unsettled funding inventory and emit the completion event itself, so read the live phase before every batch. Re-simulate whenever the batch or relevant state changes. -
Use venue-unit views for convertible records during the open window, but compare raw storage and epoch data to prove
physical coverage. Keep batches bounded, retry failed batches one ID at a time, and use raw
getQuotefor closed or non-convertible history. -
Re-scan the complete manifest. For every submitted PartyB, confirm
getRestatementInventoryProgressreports zero remaining LONG and SHORT quantity for that PartyB, and confirm both symbol-wide remaining quantity totals are zero. Independently verify that no involved PartyB was omitted. Reconcile position aggregates, funding checkpoints, and weighted aggregate funding explicitly. -
Lock Muon validity configuration. Prove that the old-basis timestamp upper bound is no later than
restatementStartedAt. If it is later, extend the operational deadline to that upper bound plus the maximum applicable validity. Wait strictly past both that deadline and the on-chainrestatementStartedAt + maxUpnlValidTime()deadline. -
Call
finalizeRestatement. If progress reportsFINALIZATION_FUNDING_RESTORATION, simulate and submit the checkpointed PartyB manifest in boundedprocessRestatementFundingbatches untilRestatementFinalized. A nonzero inventory pending count makes the initial finalization call revert. Do not treat the initial transaction's success as lifecycle completion, signature-expiry proof, or proof that no PartyB was omitted. Record itsfundingRestorationTimestamp; from that instant, all pairs must expose the fresh restated rate and unchanged crystallized snapshot whether or not their storage-restoration batch has landed. -
Verify the expected lifecycle state, explicit
1e18cumulative factor, closed window, incrementedbasisVersion, phaseNONE, zero pending checkpoint count, deleted checkpoints, unchanged crystallized snapshots, zero accumulated-rate history, rebased current rates, fresh epoch timestamps equal to the shared restoration time,SymbolAdjustment.pendingQuoteIdCutoffequal to the terminal transaction's global last quote ID, and unfreeze. Queue any remaining stale pending IDs for permissionlesscancelStalePendingQuotesbatches. - Resume Muon with raw venue prices. Reinitialize stale three-step force closes and retain the old-basis timestamp cutoff proof. Before any later validity increase, wait strictly past that timestamp bound plus the proposed applicable validity.
Compact contract reference
State and transition lookup
| Stored state | Meaning and next actions |
|---|---|
NONE = 0 |
No schedule has been stored. A manager may schedule. |
SCHEDULED = 1 |
Live before effectiveness, frozen at or after it. Manager may cancel, confirm when effective, or start direct restatement when effective. |
PRICE_ADJUSTED = 2 |
Active factor is confirmed and symbol is live unless a restatement window is open. A manager may schedule another step or start later restatement. |
APPLIED = 3 |
A physical window finalized from scheduled or price-adjusted state. This label does not prove quote coverage. |
CANCELLED = 4 |
The latest scheduled step was cancelled. Older active factors and latest schedule metadata remain stored. |
Restatement funding phase lookup
| Phase | Meaning and operator action |
|---|---|
NONE = 0 |
No funding work is pending. Outside a window, this is the terminal phase. |
FUNDING_PREPARATION = 1 |
Supply the complete involved-PartyB manifest with processRestatementFunding to snapshot open
inventory and, when active, funding. Then call completeRestatementFundingPreparation. Abort,
quote rewrites, and finalization are disabled until that completeness attestation seals the manifest.
|
QUOTE_PROCESSING = 2 |
The PartyB manifest is sealed. Rewrites are available, but finalization requires every prepared LONG and SHORT inventory counter to reach zero. |
ABORT_FUNDING_RESTORATION = 3 |
Supply checkpointed PartyB addresses until RestatementAborted; the symbol stays frozen until that
terminal transaction, and may remain frozen afterward on the direct SCHEDULED route.
|
FINALIZATION_FUNDING_RESTORATION = 4 |
Supply checkpointed PartyB addresses until RestatementFinalized; only the terminal transaction
increments basisVersion and unfreezes.
|
FUNDING_SETTLEMENT = 5 |
Settle old-basis accumulated funding for every prepared open quote with applyAdjustment. Abort
remains available, and no quote rewrite is allowed until both settlement inventory totals reach zero. The
transaction that clears both totals emits RestatementFundingSettlementCompleted and moves the
window to QUOTE_PROCESSING.
|
Global liquidation-start record
MAStorage.liquidationStartNonce is shared across all symbols and accounts, starts at zero after upgrade,
increments once for every successful new multi-step or snapshot liquidation start, and is never decremented or reset. It is
intentionally not part of SymbolAdjustment: the value protects an Operations snapshot spanning every liquidation
mode, including starts whose calldata does not enumerate symbols. Read it through getLiquidationStartNonce().
Per-symbol record, grouped by purpose
| Purpose | Fields and persistence |
|---|---|
| Latest schedule |
factor and effectiveTimestamp describe the latest step. Both are zero before the
first schedule and survive cancellation and finalization. state defaults to NONE.
scheduledCount is a lifetime count. Events use zero-based index scheduledCount - 1.
|
| Unabsorbed trading basis |
cumulativeFactor is the product of confirmed factors not yet absorbed into quote storage. Raw
zero means unset and reads as 1e18. Finalization writes explicit 1e18.
|
| Window identity and safety |
restatementEpoch increments on every successful start and survives abort and finalization.
restating is the open-window flag. restatementMutated resets to false at start,
becomes true on a quote rewrite or PartyA liquidation price snapshot, and is not cleared by finalization.
restatementFactor and restatementStartedAt are zero outside a window. Liquidation
price hashes commit to restatementEpoch and restating, so completing abort
invalidates open-window signatures without restricting a replacement snapshot's original timestamp.
|
| Preparation and funding batches |
restatementPhase, fundingCutoffTimestamp, pendingFundingPartyBCount,
and fundingRestorationTimestamp coordinate operator-supplied preparation and funding restoration
batches. Effective funding reads stop at the cutoff until abort or finalization records the shared fresh-epoch
timestamp. fundingSettlementRequired is snapshotted from the global accumulated-funding switch
when the manifest is sealed; rewrites check this snapshot, never the live switch, so flipping the switch
mid-window cannot strand a partially rewritten book.
restatementInventoryTotals[symbolId] separately records the exact prepared old-basis LONG and
SHORT quantities still unresolved, and restatementFundingSettlementTotals[symbolId] records the
subset whose funding is still unsettled. The terminal abort or finalization clears the live progress fields.
|
| Physical basis generation | basisVersion starts at zero and increments only on finalization. |
Checkpoint and layout lookup
| Field | Meaning |
|---|---|
adjustments[symbolId] |
The latest 17-field SymbolAdjustment record, not history. |
quoteRestatedEpoch[quoteId] |
Last physical rewrite epoch. Zero means never rewritten. First window epoch is one. |
adjustmentScheduledAt[symbolId] |
Block time of the latest schedule. Zero before any schedule. |
restatementFundingPartyBs[symbolId] |
Retained for storage-layout compatibility. The current workflow does not append to or read this array. |
fundingRateCheckpoints[symbolId][partyB] |
currentLongRate and currentShortRate preserve the original rates;
restatedLongRate and restatedShortRate preserve their precomputed new-basis values;
and restatementEpoch binds the record to one window. The two restated-rate fields are appended
after the legacy three fields. No checkpoint is created only when epoch duration is zero; a record with two
zero current rates is still checkpointed so its prior history cannot absorb the pause. Epoch matching rejects
stale data. Each entry is deleted by the restoration batch that processes that PartyB.
|
restatementInventoryCheckpoints[symbolId][partyB] |
Current epoch plus remaining LONG and SHORT old-basis open quantities captured when Operations submits the PartyB. Quote restatement and full close paths subtract from the corresponding side. Stale epochs are inert. |
restatementInventoryTotals[symbolId] |
Exact current-epoch LONG and SHORT quantities summed across prepared PartyBs. Finalization requires both to be zero. |
restatementFundingSettlementTotals[symbolId] |
Prepared LONG and SHORT quantities whose old-basis funding is still unsettled. Funding settlement and full closes subtract from the corresponding side; both must be zero before the first rewrite. |
quoteFundingSettledEpoch[quoteId] |
Last epoch in which the funding-only pass settled the quote. Zero means never settled by a window. |
SymbolAdjustment.pendingQuoteIdCutoff |
Highest global quote ID that existed when the latest physical restatement completed. Pending quotes at or below it are stale. Zero means no completed physical restatement has established a cutoff. |
Function lookup
| Function | Authorization and effect |
|---|---|
scheduleAdjustment |
Manager-only. Store the latest step, increment count, and record schedule block time. |
cancelAdjustment |
Manager-only. Change effective or future SCHEDULED to CANCELLED when no window is
open.
|
confirmPriceAdjusted |
Manager-only. At or after effectiveness, activate the prospective factor and unfreeze. |
startRestatement(symbolId, expectedLiquidationStartNonce) |
Manager-only. First require the expected global liquidation-start nonce, then select the factor, open an epoch, freeze, and initialize preparation progress without scanning PartyBs, symbols, positions, or liquidation flags. |
processRestatementFunding |
Manager-only. Process only the supplied PartyB addresses in the current preparation or restoration phase. The batch that reduces the pending count to zero completes abort or finalization. |
completeRestatementFundingPreparation |
Manager-only. Seal the submitted PartyB manifest and enable quote processing. |
abortRestatement |
Manager-only. After preparation is sealed, begin a fresh original-rate epoch for a mutation-free window;
complete immediately only when no checkpoint exists. Reverts during FUNDING_PREPARATION.
|
applyAdjustment |
Manager for any quote or own PartyB per quote. Settle eligible open inventory in
FUNDING_SETTLEMENT, then rewrite it in QUOTE_PROCESSING.
|
cancelPendingQuotes |
Manager-only optional cleanup. Cancel eligible pending inventory whenever its symbol is frozen. |
cancelStalePendingQuotes |
Permissionless. Cancel pending quotes whose IDs are at or below their symbol's completed physical-restatement cutoff. |
emergencyClosePosition |
Own PartyB only. While the symbol is frozen, fully close an unrestatable amount-dust quote without PartyA; Muon price and UPNLs must already use the stored quote basis. Non-dust quotes keep the ordinary emergency conditions and the freeze gate. |
setSymbolsPrice / setSymbolsPriceWithSnapshot |
Existing PartyA liquidator role. During restatement, accept venue-basis prices only when the Muon timestamp is strictly later than the window start; store the canonical price for quote-local conversion during position processing. |
liquidatePositionsPartyA / snapshot variant |
Existing PartyA liquidator role. Convert the stored venue price per quote, run ordinary PnL/funding and balance accounting, then consume old-basis inventory through the shared close helper. |
liquidatePositionsPartyB |
PARTYB_LIQUIDATOR_ROLE. During restatement, require a post-start quote-price timestamp, convert
each venue price independently, and consume inventory for each old-basis close.
|
liquidatePositionsForClearingHouse |
CLEARING_HOUSE_ROLE. Close explicit quote IDs for an active cross-PartyB liquidation or PartyA
takeover; during restatement, prices are venue-basis trusted inputs and old-basis closes consume inventory. No
counter override.
|
finalizeRestatement |
Manager-only. Require zero unresolved prepared inventory and, after strict expiry, start a fresh restated-rate epoch at one shared timestamp; materialize checkpoint storage in batches, or complete immediately when no checkpoint exists. |
All read functions are unrestricted and routed through ViewFacetSymbol;
SymbolAdjustmentFacet contains no external read selectors. getSymbolAdjustment returns raw record
values. getCumulativeFactor normalizes unset zero. getProspectiveCumulativeFactor includes a
scheduled step only in SCHEDULED. isSymbolFrozen evaluates the live predicate.
getLiquidationStartNonce returns the current global optimistic-lock sequence and must be read from the same block
as the off-chain liquidation snapshot. getSymbolAdjustment includes pendingQuoteIdCutoff.
getPendingQuoteIdCutoff returns that persistent cutoff directly, and isPendingQuoteStale combines it
with the quote's current pending status. getRestatementState returns the window flag and epoch.
getRestatementFundingProgress returns the phase, pending checkpoint count, funding cutoff, and funding
restoration timestamp. getRestatementInventoryProgress returns the current epoch, whether one PartyB was
prepared, that PartyB's remaining LONG and SHORT quantities, and the exact symbol-wide remaining LONG and SHORT quantities.
isRestatementFundingCheckpointed reports whether one PartyB has a checkpoint for the open epoch.
getQuoteRestatedEpoch returns a quote's last physical epoch.
getRestatementFundingSettlementProgress returns the current epoch, whether the window requires the funding-only
pass, and the symbol-wide LONG and SHORT quantities whose funding is still unsettled.
getQuoteFundingSettledEpoch returns the last epoch in which the funding-only pass settled a quote.
previewQuoteAdjustment selects the open-window, prospective, or active factor and returns the exact nine
converted fields.
Event lookup
AdjustmentScheduled(uint256 indexed symbolId, uint256 adjustmentIndex, uint256 factor, uint256 effectiveTimestamp)
AdjustmentCancelled(uint256 indexed symbolId, uint256 adjustmentIndex)
PriceAdjustmentConfirmed(uint256 indexed symbolId, uint256 adjustmentIndex, uint256 newCumulativeFactor)
LiquidationStartNonceIncremented(uint256 indexed nonce)
RestatementStarted(uint256 indexed symbolId, uint256 epoch, uint256 restatementFactor)
RestatementPreparationProgress(uint256 indexed symbolId, uint256 indexed epoch, uint256 submittedPartyBCount,
uint256 newlyPreparedPartyBCount, uint256 fundingCheckpointedPartyBCount,
uint256 totalRemainingLongAmount, uint256 totalRemainingShortAmount,
uint256 pendingFundingPartyBCount)
RestatementPreparationCompleted(uint256 indexed symbolId, uint256 indexed epoch,
uint256 totalRemainingLongAmount, uint256 totalRemainingShortAmount,
uint256 pendingFundingPartyBCount, RestatementPhase phase)
RestatementFundingSettlementCompleted(uint256 indexed symbolId, uint256 indexed epoch)
RestatementInventoryPrepared(uint256 indexed symbolId, uint256 indexed epoch, address indexed partyB,
uint256 partyBRemainingLongAmount, uint256 partyBRemainingShortAmount,
uint256 totalRemainingLongAmount, uint256 totalRemainingShortAmount)
RestatementInventoryConsumed(uint256 indexed symbolId, uint256 indexed epoch, uint256 indexed quoteId,
address partyB, PositionType positionType, uint256 consumedAmount)
RestatementFundingRestorationStarted(uint256 indexed symbolId, uint256 indexed epoch, bool finalizing,
uint256 pendingPartyBs)
RestatementFundingRestorationProgress(uint256 indexed symbolId, uint256 indexed epoch, bool finalizing,
uint256 processedPartyBs, uint256 remainingPartyBs)
RestatementAborted(uint256 indexed symbolId, uint256 epoch)
QuoteAdjusted(uint256 indexed quoteId, uint256 indexed symbolId, uint256 epoch, uint256 factor,
uint256 oldQuantity, uint256 newQuantity, uint256 oldOpenedPrice, uint256 newOpenedPrice)
PendingQuoteCancelledByAdjustment(uint256 indexed quoteId, uint256 indexed symbolId)
PendingQuoteIdCutoffUpdated(uint256 indexed symbolId, uint256 indexed epoch, uint256 cutoffQuoteId)
StalePendingQuoteCancelled(uint256 indexed quoteId, uint256 indexed symbolId, uint256 cutoffQuoteId)
RestatementFinalized(uint256 indexed symbolId, uint256 epoch)
Storage retains only the latest schedule, so indexers need these events for complete adjustment history.
LiquidationStartNonceIncremented is emitted from the shared Diamond address once per successful PartyA, isolated
PartyB, or cross-PartyB liquidation start. Pair it with the route's ordinary liquidation event to identify the liquidation;
the nonce event itself intentionally carries no symbol or account. Its value never decreases, and unrelated-symbol and
single-step starts still advance it. Preparation events distinguish PartyB address counts from LONG and SHORT position
quantities, report each fresh inventory snapshot, summarize every batch, and record the exact quantities sealed for quote
processing. Each inventory-consumption event identifies the quote, side, and old-basis quantity consumed. Starting from the
per-PartyB snapshots, indexers subtract these side-specific deltas to reconstruct the same exact PartyB and symbol remainders
enforced on-chain, without a second completion log. A liquidation close of an already-restated quote emits no
inventory-consumption event because it has already left the old-basis counter. PartyA price-setup and snapshot events expose
the venue-basis payload value during an open window; close hooks, average-closed-price fields, PartyA funding/PnL attribution,
and Clearing House trade volume use the quote's converted stored price. Indexers should normalize the closed quote before
comparing those values with the venue payload. Funding-restoration events report restoration mode and remaining PartyB work.
Per-pair preparation and restoration also emit the existing AccumulatedFundingStateUpdated. When checkpoints
exist, RestatementAborted or RestatementFinalized is emitted by the last funding batch rather than
the transaction that starts restoration. That terminal transaction also emits PendingQuoteIdCutoffUpdated. Each
later lazy cancellation emits StalePendingQuoteCancelled. During FUNDING_SETTLEMENT, every
successfully processed quote emits QuoteFundingSettled, including when the amount is zero. During the later
QUOTE_PROCESSING pass, every successfully rewritten quote emits QuoteAdjusted. Indexers need both
events to attribute settlement side effects and the later unit conversion.