> For the complete documentation index, see [llms.txt](https://docs.symm.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.symm.io/security-and-architecture/muon-architecture.md).

# Muon Architecture

Muon is the off-chain computation and signing layer that feeds Symmio with verified data. Every state-changing operation that depends on an external market passes through Muon. The off-chain TSS network calculates the data, signs it, and a gateway node relays it; the on-chain protocol verifies both the threshold signature and the gateway's ECDSA signature before accepting the call.

In our current version the Muon layer keeps the same dual-signature model but pulls its trust set out of the diamond. Instead of one TSS public key and one gateway address baked into MuonStorage, the protocol now consults a standalone MuonSignatureVerifier contract that supports multiple keys, multiple gateway signers, key rotation without downtime, and per-category authorization.&#x20;

### Why Symmio needs an oracle at all

Symmio is a bilateral derivatives protocol. At any moment, the contract needs to know:

* Current prices for the trading symbols involved.
* Unrealized PnL for PartyA and PartyB.
* Whether each party is solvent enough to execute the requested action.

Calculating UPNL on-chain would require iterating every open position and fetching every price which would be expensive to do on most networks. Muon performs the calculation off-chain and ships back a signed attestation that the contracts verify cheaply. The protocol's correctness rests on two things: that the math the network performs is correct, and that the signatures can't be forged or replayed. The dual-layer verification model exists to make the second condition robust to a partial compromise.

### Dual-layer signature verification

Symmio doesn't trust a single signer. Every Muon signature passes through two independent layers before the contract accepts it.

#### Layer 1: TSS (Threshold Signature Scheme)

Muon operates a distributed network of nodes. When a signature request comes in, multiple nodes independently fetch price data, calculate the requested values (UPNL, solvency, etc.), and contribute to a threshold signature. The result is a Schnorr signature that proves a threshold of nodes agreed on the data. The contract verifies this Schnorr signature against the registered TSS public keys.

In the current version (0.8.5), the verification iterates over all registered public keys in MuonSignatureVerifier. If any one of them validates the Schnorr signature, the TSS check passes. This is what makes seamless key rotation possible; old and new keys can coexist during a transition window with no downtime.

#### Layer 2: Gateway ECDSA

After the TSS check, the contract recovers the signer from a standard ECDSA signature on the same hash and matches it against the list of registered gateway signers. The gateway is the Muon node that relayed the TSS-signed data to the user. The second signature is what prevents a compromised TSS key from being exploited without simultaneously compromising a gateway node.

### Per-category authorization

After a matching TSS key or gateway signer is found, the verifier also checks that the key/signer is explicitly authorized for the MuonFunction category being verified. The categories are:

```solidity
enum MuonFunction {
    Trading,            // SendQuote, LockQuote, OpenPosition(s), FillCloseRequest(s), EmergencyClosePosition, ClosePositions
    AccountManagement,  // Deallocate, SafeDeallocate, DeallocateForPartyB, TransferAllocation
    Settlement,         // SettleUpnl, SettleUpnlUnified
    ForceClose,         // ForceClose, InitializeForceClose, SettleUpnlForForceClose(Legacy), FinalizeForceClose
    Funding,            // ChargeFundingRate, ChargeAccumulatedFundingFee
    LiquidationPartyA,  // LiquidatePartyA, SetSymbolsPrice, DeferredLiquidatePartyA, DeferredSetSymbolsPrice
    LiquidationPartyB   // LiquidatePartyB, LiquidatePositionsPartyB
}
```

A key authorized only for Trading cannot produce valid signatures for LiquidationPartyA. Permissions are opt-in, so newly added keys and gateway signers start with no permissions and cannot validate any signatures until explicitly granted via `setPublicKeyPermissions` or `setGatewaySignerPermissions` (both restricted to `SETTER_ROLE` on the verifier).&#x20;

### The MuonSignatureVerifier contract

The verifier lives at `contracts/helpers/verification/SymmioSignatureVerifier.sol` (the contract itself is named `MuonSignatureVerifier`) and is deployed as an independent contract, not part of the diamond. The diamond stores its address in `GlobalAppStorage.signatureVerifier` and `LibMuon.verifyTSSAndGateway` delegates to it. The interface:

```solidity
// contracts/core/interfaces/IMuonSignatureVerifier.sol
enum MuonFunction {
    Trading,
    AccountManagement,
    Settlement,
    ForceClose,
    Funding,
    LiquidationPartyA,
    LiquidationPartyB
}

interface IMuonSignatureVerifier {
    struct PublicKey { uint256 x; uint8 parity; }
    struct SchnorrSign {
        uint256 signature;
        address owner;
        address nonce;
    }

    // Signature verification
    function verify(bytes32 hash, SchnorrSign memory sign, bytes calldata gatewaySignature, MuonFunction func) external view;
    function verify(bytes32 hash, SchnorrSign memory sign, bytes calldata gatewaySignature) external view; // no auth check

    // Public key management
    function addPublicKey(PublicKey memory pubKey) external;
    function removePublicKey(PublicKey memory pubKey) external;
    function getAllPublicKeys() external view returns (PublicKey[] memory);

    // Gateway signer management
    function addGatewaySigner(address signer) external;
    function removeGatewaySigner(address signer) external;
    function getAllGatewaySigners() external view returns (address[] memory);

    // Per-function authorization
    function setPublicKeyPermissions(PublicKey memory pubKey, MuonFunction[] calldata functions, bool allowed) external;
    function setGatewaySignerPermissions(address signer, MuonFunction[] calldata functions, bool allowed) external;
    function isPublicKeyAuthorized(PublicKey memory pubKey, MuonFunction func) external view returns (bool);
    function isGatewaySignerAuthorized(address signer, MuonFunction func) external view returns (bool);
}
```

### Verification flow inside the contract

```solidity
	/// @notice Verifies both the TSS Schnorr signature and the gateway ECDSA signature,
	///         and checks that both the signing key and gateway are authorized for the given category
	/// @param hash The hash of the signed data
	/// @param sign The Schnorr signature to verify against registered public keys
	/// @param gatewaySignature The ECDSA gateway signature to verify
	/// @param func The operation category requesting verification
	function verify(bytes32 hash, SchnorrSign memory sign, bytes memory gatewaySignature, MuonFunction func) external view {
		// Verify TSS via Muon
		bool verifiedTSS = false;
		for (uint256 i = 0; i < publicKeys.length; i++) {
			if (LibMuonV04ClientBase.muonVerify(uint256(hash), sign, publicKeys[i])) {
				require(publicKeyPermissions[_publicKeyId(publicKeys[i])][func], "MuonSignatureVerifier: Key not authorized for function");
				verifiedTSS = true;
				break;
			}
		}
		require(verifiedTSS, "MuonSignatureVerifier: TSS not verified");

		// Verify Gateway Signature
		address signer = hash.toEthSignedMessageHash().recover(gatewaySignature);
		bool gatewayVerified = false;
		for (uint256 i = 0; i < gatewaySigners.length; i++) {
			if (signer == gatewaySigners[i]) {
				require(gatewaySignerPermissions[signer][func], "MuonSignatureVerifier: Gateway not authorized for function");
				gatewayVerified = true;
				break;
			}
		}
		require(gatewayVerified, "MuonSignatureVerifier: Gateway is not valid");
	}
```

### Key rotation procedure

1. `addPublicKey(newKey)` on the verifier.
2. setPublicKeyPermissions(newKey, \[...], true) to authorize it for the required functions.
3. Both old and new keys now validate Schnorr signatures — no downtime.
4. Once all Muon nodes have switched to the new key, removePublicKey(oldKey).

The same flow applies independently to `gatewaySigners`.

### What gets signed

Different operations require different signature payloads. Two things are worth separating clearly:

* **Struct fields**: what the caller actually passes in the `Sig` struct (defined in `MuonStorage.sol`). Every struct carries `reqId`, `timestamp`, a `gatewaySignature`, and the `sigs` (SchnorrSign), plus the UPNL/price values listed below.
* **Hashed inputs**: the contract reconstructs the signed `keccak256` hash inside the `LibMuon*` functions. The hash additionally binds values that are **not** struct fields: `muonAppId`, `address(this)`, the relevant party address(es), the on-chain `nonce`, `chainId`, and (for price sigs) the `symbolId`. These come from chain state or function arguments, not from the struct.

#### SingleUpnlSig

Used for: `deallocate()` (MuonFunction.AccountManagement), `deallocateForPartyB()` / `transferAllocation()`, `liquidatePartyB()` (MuonFunction.LiquidationPartyB).

```solidity
struct SingleUpnlSig {
    bytes reqId;
    uint256 timestamp;
    int256 upnl;
    bytes gatewaySignature;
    IMuonSignatureVerifier.SchnorrSign sigs;
}
```

#### SingleUpnlAndPriceSig

Used for: `sendQuote()`. This is the only signature `sendQuote` accepts.

```solidity
struct SingleUpnlAndPriceSig {
    bytes reqId;
    uint256 timestamp;
    int256 upnl;
    uint256 price;
    bytes gatewaySignature;
    IMuonSignatureVerifier.SchnorrSign sigs;
}
```

#### SingleUpnlWithPendingBalanceSig

Muon signature attesting to a party's UPNL plus their pending withdrawal balance.

```solidity
struct SingleUpnlWithPendingBalanceSig {
    bytes reqId;
    uint256 timestamp;
    int256 upnl;
    uint256 pendingBalance; // their pending withdrawal balance
    bytes gatewaySignature;
    IMuonSignatureVerifier.SchnorrSign sigs;
}
```

#### PairUpnlSig

Used for: funding operations: `chargeFundingRate()` and `chargeAccumulatedFundingFee()`.&#x20;

```solidity
struct PairUpnlSig {
    bytes reqId;
    uint256 timestamp;
    int256 upnlPartyA;
    int256 upnlPartyB;
    bytes gatewaySignature;
    IMuonSignatureVerifier.SchnorrSign sigs;
}
```

#### PairUpnlAndPriceSig

Used for: `openPosition()`, `fillCloseRequest()`, and `emergencyClosePosition()` (MuonFunction.Trading). PairUpnlSig data plus a single symbol `price`; `symbolId` is supplied as a function argument and enters the hash.

```solidity
struct PairUpnlAndPriceSig {
    bytes reqId;
    uint256 timestamp;
    int256 upnlPartyA;
    int256 upnlPartyB;
    uint256 price;
    bytes gatewaySignature;
    IMuonSignatureVerifier.SchnorrSign sigs;
}
```

#### PairUpnlAndPricesSig (batch)

Used for: `openPositions()`, `fillCloseRequests()` (MuonFunction.Trading). Carries arrays of `symbolIds` and `prices` for the batched quotes, but the UPNL values are two aggregates per party (`upnlPartyA`, `upnlPartyB`).

```solidity
struct PairUpnlAndPricesSig {
    bytes reqId;
    uint256 timestamp;
    int256 upnlPartyA;
    int256 upnlPartyB;
    uint256[] symbolIds;
    uint256[] prices;
    bytes gatewaySignature;
    IMuonSignatureVerifier.SchnorrSign sigs;
}
```

#### LiquidationSig

Used for: `liquidatePartyA()` and `setSymbolsPrice()` (MuonFunction.LiquidationPartyA). **Not** used for PartyB liquidation — `liquidatePartyB()` uses `SingleUpnlSig`.

```solidity
struct LiquidationSig {
    bytes reqId;
    uint256 timestamp;
    bytes liquidationId;
    int256 upnl;
    int256 totalUnrealizedLoss;
    uint256[] symbolIds;
    uint256[] prices;
    bytes gatewaySignature;
    IMuonSignatureVerifier.SchnorrSign sigs;
}
```

### Replay protection via nonces

Muon signatures include two distinct identifiers that serve different purposes.

#### reqId (Muon Request ID)

Each Muon request generates a unique reqId that's included in the signed hash.

#### Contract nonces

The contract maintains nonce counters in AccountStorage for each PartyA / PartyB. Muon reads the current on-chain nonce when it signs, includes it in the hash, and the contract verifies that the signed nonce matches the current on-chain value. After a successful state-changing call, the relevant nonce increments making any older signature invalid.

```
1. Muon reads current on-chain nonce (e.g. 5)
2. Muon signs data including that nonce
3. User submits the signature to the contract
4. Contract verifies hash includes nonce = 5 ✓
5. State change ➜ nonce increments to 6
```

In the current version, cross-mode (cross-PartyB) solvers will automatically sign with nonce = 0 for parallel operations. Sequential nonces remain available where ordering matters.

### Signature expiration

Signatures have a limited validity window defined by upnlValidTime in `MuonStorage`. This prevents an old signature from being reused after market conditions have moved. The window is measured against the timestamp field in the signed payload, not the block timestamp at submission, so a signed message that sits in a mempool for too long simply expires rather than landing on stale state.

### Oracle-less trading: bypassing Muon for bound pairs

When a PartyA trades exclusively with a single PartyB, the dual-Muon trust path is overkill. A malicious PartyB can't harm a different PartyA because there is no different PartyA. The current version lets a PartyA opt into this by binding to a specific PartyB via bindToPartyB. Once bound (and while that PartyB is bindable), the contract skips both the Muon signature check and the on-chain solvency check for the trading path against that PartyB.

Binding is also the gate for instant trading: `activateInstantActionMode` requires `BindStatus.BOUND`.&#x20;

In bound mode the trading functions still take the full `Sig` struct, and they still read the numeric fields. What is not used are the cryptographic fields (`reqId`, `gatewaySignature`, and the `sigs` Schnorr signature), since the `verify` call is skipped entirely; callers leave those empty. So no valid Muon signature is required.

Binding requires a clean state:

* No pending quotes (zero pending locked balance).
* No open positions with any other PartyB.

Unbinding is a soft, time-windowed flow:

* `requestToUnbindFromPartyB` — moves the relationship to `PENDING_UNBIND`, records a timestamp.
* `cancelUnbindRequest` — restore to `BOUND` if the user changes their mind.
* `completeUnbindRequest` — finishes the unbind. PartyB can complete immediately. Anyone else (including PartyA) must wait out the cooldown.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.symm.io/security-and-architecture/muon-architecture.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
