This SIP proposes upgrades to Perps v2 to be deployed on Synthetix v3 with significantly improved features, usability, and operational resilience.
This SIP proposes deployment of a perpetual futures market on Synthetix v3. Core upgrades include: (1) native cross-margining of positions, (2) multi-collateral margin support, (3) revamped liquidations, and (4) improved deterministic settlement.
Despite achieving moderate levels of success since launching with ~$150M of open interest and over $20B of trading volume to date, Perps v2 supports a relatively limited set of features (isolated margin / USD margin only). Secondly, order settlements in v2 are fragile and highly susceptible to disruptions in sequencer inclusion latency. Lastly, v2 relies on suboptimal components such as the endorsed liquidator introduced in SIP-2005 which should be phased out in favor of more distributed mechanisms.
Perps v3 proposes to address the challenges described above using several key architectural features including (1) user accounts for aggregated spot and perps market positions, (2) supermarkets for grouped markets with shared LPs, (3) partial liquidations for gradual derisking, (4) retroactive pricing for asynchronously settled orders.
Cross-margin and multi-collateral accounts
At a high level, cross-margin and multi-collateral support are simplified via user accounts represented by an NFT. To ensure homogenous distribution of risk to LPs throughout the cross margin system, individual markets are aggregated into a cross-marginable supermarket.
SIP-2005 introduced endorsed liquidation keepers to perps v2 to ensure that large positions could be liquidated in an MEV-resistant manner (e.g. sandwiching of liquidation transactions). In Perps v3, large positions are gradually liquidated one non-sandwichable portion at a time with a configurable delay in between partial liquidations. This eliminates profit motives to sandwich liquidation transactions, and allows for balancing arbitrages in between partial liquidations.
Delayed orders in Perps v2 have proven extremely effective in minimizing toxic flow, however the details of the implementation can be improved to produce a more resilient settlement flow. In short, the v2 flow requires a price update from after a configurable delay elapses to settle an order. During times of network congestion, this can result in orders being settled with prices from 30-60 seconds post commitment. In perps v3, the settlement process enforces that the execution price is the price at commitment time and are executed (settled) in the available window (commitment + delay, commitment + delay + window length)
Once a proxy is created, the first step is to call
initializeFactory which will register the super market with the core system. The
superMarketId is then stored and used for future deposits and withdrawals from the core system as debt and credit is accrued.
Each perps supermarket has support for accounts natively. Each account is an NFT and has support for Role Based Access Control. The current permissions that are supported are:
- commitOrder Note: Only the owner of the NFT or an address with permission to perform the above actions on behalf of the owner can perform the action.
Perps v3 supports multiple collateral types. Currently, we only support synths that are registered with the v3 spot market. This is done so that it’s easy for the perps market to have liquidity to sell synths when required, and allows it to easily get quotes to determine margin value.
Each collateral type can be configured by setting the max allowed amount for the supermarket. By setting this value to a non-zero value, the collateral becomes a valid collateral for traders to deposit as margin up-to the max value.
Traders are allowed to open multiple positions for different markets within the supermarket, limited to one position per market. The trader’s account margin value fluctuates based on the PNL fluctuations of each open position.
Traders follow a commit/settle pattern similar to the Spot market to confirm orders. A trader can only have one position per supported market in the supermarket. Initially, the contracts also enforce only one pending order at any given time for an account. For an order to be confirmed, it has to pass the following validation checks:
- Trader’s account is not eligible for liquidation already. (See criteria for liquidations in the liquidations section)
- Trader has enough margin in the account, after factoring in all current open position’s PNL, and the immediate PNL (if negative) due to fill price, to cover the order fees, the initial margin requirement for all trader positions plus the newly requested position, and any liquidation rewards they might have to pay out in case of liquidation of the position.
Each market can be configured with a set of settlement strategies. These are similar to the ones in the spot market and have all the information for how the orders will settle. The initial supported strategy type is
Pyth, but we’ve future proofed it to add any other oracle provider and strategy, as needed.
A valid order request has the following properties:
uint128 marketId; uint128 accountId; int128 sizeDelta; uint128 settlementStrategyId; // used for slippage protection, in case the market skew produces a fill price that has too far deviated from the trader’s expectation. uint256 acceptablePrice; bytes32 trackingCode; // if configured beforehand, will receive the configured portion of the order fees. address referrer;
When an order is committed, there’s a window in which the order is required to be settled after which the order is considered expired. If the order is expired, only then may a trader commit a new order. If there’s a pending order, the trader is not allowed to modify their margin or commit another order.
A keeper can cancel an order if the price at commitmentTime is worse than the acceptable price set during commitment by the trader. The cancellation can only occur within the settlement window.
The settlement start time is defined as
commitmentTime + settlementDelay, and the end time is
commitmentTime + settlementDelay + settlementDuration. The
settlementDuration are pulled from the settlement strategy. Orders may only be settled while they are within this settlement window as defined above, with the benchmark price at commitment time.
A trader must meet certain margin requirements to open new positions, and ensure the account is not eligible for liquidations. The following configurable values are used to determine these aforementioned margins:
uint256 initialMarginRatioD18; uint256 maintenanceMarginScalarD18; uint256 minimumInitialMarginRatioD18; flagRewardRatioD18;
To open a new position, the trader must meet an initial margin requirement. The calculation for this requirement is as follows:
impactOnSkew = |size| / skewScale initialMarginRatio = (impactOnSkew * initialMarginRatioD18) + minimumInitialMarginRatioD18
initialMarginRatio is multiplied by the notional value of the requested position to determine the initial margin the trader would require in their account as margin. This validation happens both on order commitment and settlement.
When an account already has positions open, different actions, including
liquidateAccount will check to see whether the trader’s available margin is under the maintenance margin threshold.
Maintenance margin is a factor smaller than the initial margin requirement, and so the calculation is as follows:
maintenaceMarginRatio = initialMarginRatio * maintenanceMarginScalarD18
maintenanceMarginRatio is multiplied by the notional value of the size to get the required maintenance margin for the position. The account is considered eligible for liquidation if the sum of all position’s maintenance margins is greater than the available trader account margin.
There is another margin requirement at play in determining liquidation threshold and that’s the liquidation reward margin. If a position is flagged for liquidation, the liquidator receives a reward for their service, and that’s determined by the following equation:
liquidationReward = notionalValue * liquidationRewardRatioD18
The flagger receives all liquidation rewards across all positions of the account capped at a max that's a factor of the margin. More on that later.
This keeper flagger rewards and liquidation rewards are added to both the initial and maintenance margins to ensure the trader has enough margin to cover the keeper/reward cost.
As mentioned in the above section, an account is eligible for liquidation if the account’s available margin is less than the combined maintenance margins of all its positions.
When this criteria is met, a liquidator may call the
liquidateAccount function that will begin the liquidation process:
The first step is the account being flagged which triggers selling all of their collateral that’s comprised of synths using the spot market, converting them into sUSD, and depositing it back into the core system. This ensures the core system is not holding the trader’s synths as collateral, which can be subject to fluctuations in price.
The account’s positions are then either partially or fully closed. A partial liquidation can happen based on the size of the position and the following configurable parameters for each market:
uint256 maxSecondsInLiquidationWindow; uint256 maxLiquidationLimitAccumulationMultiplier;
The goal of this is to slow down the size of the liquidations happening within a market in a given time window. The size of allowed liquidation for each second is calculated as:
maxLiquidationPerSecond = (makerFee + takerFee) * skewScale * maxLiquidationLimitAccumulationMultiplier; maxLiquidationSize = maxLiquidationPerSecond * maxSecondsInLiquidationWindow;
maxLiquidationSize is the max amount of the market that can be liquidated within the configured window. This mitigates any front-running possibilities where other actors can take advantage of the price impact due to the large swing in skew that results from a large size liquidation.
If an account hits a limit, and can only be partially liquidated, the account stays in the
flaggedForLiquidation array. Liquidation keepers can call the
liquidateAccount after the configured # of seconds in the window, to continue the account liquidation.
liquidateFlagged will iterate over all partially liquidated accounts and try to liquidate more of its positions.
If an account has no open positions, then it’s considered fully liquidated, and is then reset. Once reset, the trader can add new margin and start trading again.
There's two rewards that are paid out to ensure keepers are incentivized appropriately and all accounts are efficiently liquidated:
flaggerReward: reward for flagging an account for liquidation
liquidationReward: reward for liquidating size of position until 0 and is dependent on the # of liquidation windows required.
The reward is paid out using the account’s margin and is accounted for when calculating the required margins.
As we move towards offchain oracle providers, the traders are responsible for paying for the cost of price feed updates. Liquidation and settlement keepers who perform the price updates get refunded via traders margins and is accounted for when determining margin requirements. To lower the burden on the trader, the contracts support optionality in how often the price feeds need to be updated.
There's two staleness tolerances currently supported which governs when a price is too stale:
- Default tolerance configured on the price feed node. (i.e 30 minutes)
- Strict tolerance configurable for every perps market (i.e 30 seconds)
The expectation is that the default tolerance is used for all transactions except for account liquidations in which case the strict tolerance is utilized.
Market Debt/Credit System
The supermarket reports debt to the Core system on a continuous basis by calculating the total trader’s PNL across all markets in the supermarket.
When the trader deposits margin, it is automatically deposited into the core system and the supermarket tracks the trader’s collateral amounts. The trader’s total collateral value is added to the reported debt to ensure LPs can’t realize this value until either a trader loses money, or is liquidated. As the system deducts from trader’s accounts the credit is automatically realized by the LPs when their total collateral value is deducted on the supermarket system.
During liquidations, all synths associated to the liquidated account get sold using the spot market and its corresponding value is deposited as credit into the core system in sUSD.
taker fees operate exactly like in Perps v2. Both are configured per market and based on the skew, one of them is applied to the order’s notional value.
Nothing has changed from perps v2 regarding funding. Each position accrues funding and is added to the PNL of a position which is used to determine the trader’s available margin.
Global Perps Market Configuration : applied globally to all markets in the super market
/** * @dev fee collector contract * @dev portion or all of the order fees are sent to fee collector contract based on quote. */ IFeeCollector feeCollector; /** * @dev Percentage share of fees for each referrer address */ mapping(address => uint256) referrerShare; /** * @dev mapping of configured synthMarketId to max collateral amount. * @dev USD token synth market id = 0 */ mapping(uint128 => uint) maxCollateralAmounts; /** * @dev when deducting from user's margin which is made up of many synths, this priority governs which synth to sell for deduction */ uint128 synthDeductionPriority; /** * @dev minimum configured keeper reward for the sender who liquidates the account */ uint128 maxPositionsPerAccount; /** * @dev maximum configured number of concurrent collaterals per account. * @notice If set to zero it means no new collaterals can be added accounts, but existing collaterals can be increased or decreased. * @notice If set to a larger number (larger than number of collaterals enabled) it means is unlimited. */ uint128 maxCollateralsPerAccount;
Market Configuration: applied to each specific market
OrderFee.Data orderFees; SettlementStrategy.Data settlementStrategies; uint256 maxMarketSize; // oi cap uint256 maxFundingVelocity; uint256 skewScale; /** * @dev the initial margin requirements for this market when opening a position * @dev this fraction is multiplied by the impact of the position on the skew (open position size / skewScale) */ uint256 initialMarginRatioD18; /** * @dev This value gets applied to the initial margin ratio to ensure there's a cap on the max leverage regardless of position size */ uint256 minimumInitialMarginRatioD18; /** * @dev This scalar is applied to the calculated initial margin ratio * @dev this generally will be lower than initial margin but is used to determine when to liquidate a position * @dev this fraction is multiplied by the impact of the position on the skew (position size / skewScale) */ uint256 maintenanceMarginScalarD18; /** * @dev This ratio is multiplied by the market's notional size (size * currentPrice) to determine how much credit is required for the market to be sufficiently backed by the LPs */ uint256 lockedOiRatioD18; /** * @dev This multiplier is applied to the max liquidation value when calculating max liquidation for a given market */ uint256 maxLiquidationLimitAccumulationMultiplier; /** * @dev This configured window is the max liquidation amount that can be accumulated. * @dev If you multiply maxLiquidationPerSecond * this window in seconds, you get the max liquidation amount that can be accumulated within this window */ uint256 maxSecondsInLiquidationWindow; /** * @dev This value is multiplied by the notional value of a position to determine flag reward */ uint256 flagRewardRatioD18; /** * @dev minimum position value in USD, this is a constant value added to position margin requirements (initial/maintenance) */ uint256 minimumPositionMargin; /** * @dev Threshold for allowing further liquidations when max liquidation amount is reached */ uint256 maxLiquidationPd; /** * @dev if the msg.sender is this endorsed liquidator during an account liquidation, the max liquidation amount doesn't apply. * @dev this address is allowed to fully liquidate any account eligible for liquidation. */ address endorsedLiquidator; /** * @dev the price feed id for the market. this node is processed using the oracle manager which returns the price. * @dev the staleness tolerance is provided as a runtime argument to this feed for processing. */ bytes32 feedId; /** * @dev strict tolerance in seconds, mainly utilized for liquidations. */ uint256 strictStalenessTolerance;