Technical Integration


This guide is for new integrators starting from scratch. If you have an existing v1 integration and want to migrate to V2 then refer please to FuturesV1 -> PerpsV2 (Mintaka).


All PerpsV2 contracts are routed proxies. That is, for the most part, there's a single entry point to perform most of your integration needs. You can see all available PerpsV2 contracts on GitHub by filtering by PerpsV2 prefix or infix on .sol file names (see GitHub). Here is a quick snapshot:
> tree contracts | grep 'PerpsV2*'
├── MixinPerpsV2MarketSettings.sol
├── PerpsV2ExchangeRate.sol
├── PerpsV2Market.sol
├── PerpsV2MarketBase.sol
├── PerpsV2MarketData.sol
├── PerpsV2MarketDelayedOrders.sol
├── PerpsV2MarketDelayedOrdersBase.sol
├── PerpsV2MarketDelayedOrdersOffchain.sol
├── PerpsV2MarketProxyable.sol
├── PerpsV2MarketSettings.sol
├── PerpsV2MarketState.sol
├── PerpsV2MarketViews.sol
├── ProxyPerpsV2.sol
There are 3 primary points of interaction: ProxyPerpsV2, PerpsV2MarketSettings, and FuturesMarketManager (note: does not have a PerpsV2 prefix/suffix).


Each PerpsV2 market is deployed as a distinct instance of the ProxyPerpsV2 contract, uniquely identified by a marketKey. Below are all available PerpsV2 markets:


Market Key

Optimism Goerli

Due to contract size limitations, we've designed ProxyPerpsV2 to be a router of sorts. The router proxies calls downstream to a collection of registered targets, which can be described by calling getAllTargets on ProxyPerpsV2. Although an implementation detail and likely not impactful for integration, it may helpful to understand how these contracts link together.
In the past, we've found integrating with proxy contracts can sometimes be difficult to work with as definitions are scattered across an array of different targets. For simplicity, we've defined a consolidated interface for ABI generation which can be found here.


As the name would suggest, it's simply just a management contract for markets available on perps. It's a top-level contract that provides a holistic scan of all available markets, consolidated views to query data in aggregate, and acts as a registry to track new markets as they are deployed.
Although this does not provide mutative methods to perform actions externally (as mutative operations are applied on the market level), it may be useful during debugging/integration to retrieve metadata for either display or inference or even to help avoid hardcoded values in your keepers.


See market settings page for details.

Contract Addresses

As of writing this, PerpsV2 is only available on Optimism.

How to Transfer Margin

All PerpsV2 markets have isolated margin. Position A on the ETH market will not affect position B on the BTC market. This is important as it means margin must always be transferred before positions can be opened on new markets.
interface IPerpsV2Market {
function transferMargin(int marginDelta) external;
const market = new Contract(address, abi, signer);
const marginDelta = BigNumber.from(100);
await market.transferMargin(marginDelta);
A positive marginDelta is considered a deposit, negative is a withdrawal and zero is a no-op. To ensure users do not receive revert errors refer to the minInitialMargin on the minimum required margin a position must have.

How to Open a Position

There are 3 methods to open a position: Atomically orders (not to be confused with atomic swaps), delayed orders, and delayed off-chain orders.

Atomic orders

As the name would suggest, atomic orders allow users to open, close, or modify a position atomically (i.e. within a single transaction and without the need for keepers). Historically, this was the primary method for users to open a position in V1.
However, with the introduction of hybrid oracles, it's recommended to use delayed off-chain orders (see below). Prices update more frequently and fees are drastically lower. Below is the atomic orders interface with modifyPosition being the operation to open atomically.
interface IAtomicOrders {
function modifyPosition(int sizeDelta, uint priceImpactDelta) external;
function modifyPositionWithTracking(
int sizeDelta,
uint priceImpactDelta,
bytes32 trackingCode
) external;
function closePosition(uint priceImpactDelta) external;
function closePositionWithTracking(uint priceImpactDelta, bytes32 trackingCode) external;
Similar to transferMargin, we use the sign to indicate whether we want to modify long (positive) or short (negative). Atomic orders have a convenience method to close out the entire position without specifying a sizeDelta.
Please refer to the section below for more details on priceImpactDelta and trackingCode.
Again, it is highly recommended to NOT use this method to open a position. Instead, use an off-chain orders or delayed orders instead. The fees will be substantially lower with other methods.

Delayed orders

Delayed orders are async time based orders. Rather than opening a position with a single transaction, an order is created in one transaction and then executed in a separate transaction in the future, hence async and delayed. Positions opened through async delayed orders are charged significantly less than atomic orders. The interface can be found below:
interface IDelayedOrders {
function submitDelayedOrder(
int sizeDelta,
uint priceImpactDelta,
uint desiredTimeDelta
) external;
function submitDelayedOrderWithTracking(
int sizeDelta,
uint priceImpactDelta,
uint desiredTimeDelta,
bytes32 trackingCode
) external;
function cancelDelayedOrder(address account) external;
function executeDelayedOrder(address account) external;
Order fees are based on order type, size, and side (long/short). You can calculate the order fee by passing a sizeDelta and an orderType (atomic, delayed, delayed off-chain) to the orderFees view.
Order submission requires similar arguments to that of modifyPosition with the addition of a desiredTimeDelta argument. desiredTimeDelta specifies the amount of time (in seconds) an async order must wait before it is executable. If desiredTimeDelta == 0 then we default to the minimum required time for convenience. The specified desiredTimeDelta must be above the required minimum otherwise a revert will occur. The minimum can be found in PerpsV2MarketSettings.
Once an order has been submitted successfully, it's stored and tagged with the current block's timestamp + desiredTimeDelta (denoted as intentionTime) for execution at a later time. After block.timestamp >= order.intentionTime then the order is executable by either the account that submitted the order or a keeper.
Orders can go stale. We consider an order stale if it hasn't been executed for a long enough period. Stale orders can no longer be executed and can only be cancelled.
A commitment fee is charged on order submission. This is deducted from the trader's margin. If an order is cancelled, the commitment fee is foregone to the fee pool and rewarded to SNX stakers.
A keeper fee is also charged on order submission however completely refunded if the executing account is the same as the submitter.
Delayed orders are also executable before the intention time. This typically occurs during high price volatile periods. If a price update occurs

Delayed off-chain orders

Similar to DelayedOrders, off-chain orders also follow a familiar interface and operate asynchronously. From an integration standpoint, this is almost exactly the same as delayed orders (with the exception of differing function names and execution).
interface IDelayedOffchainOrders {
function submitOffchainDelayedOrder(int sizeDelta, uint priceImpactDelta) external;
function submitOffchainDelayedOrderWithTracking(
int sizeDelta,
uint priceImpactDelta,
bytes32 trackingCode
) external;
function cancelOffchainDelayedOrder(address account) external;
function executeOffchainDelayedOrder(address account, bytes[] calldata priceUpdateData) external payable;
Since off-chain orders require off-chain prices, price feed update data needs to be submitted upon execution. Off-chain prices are queried against Pyth and passed into executeOffchainOrder as a bytes[] array. On-chain we perform a variety of checks and if all pass, the order is executed and a position is opened.
If the Pyth off-chain price deviates too far from on-chain prices, the execution will revert and a position will not be opened. This deviation can also be found in PerpsV2MarketSettings.
Not all markets have off-chain delayed orders enabled due to needing both Pyth and Chainlink price feeds.
Executing or canceling an off-chain order using a non-off-chain method will result in revert. The inverse is also true.
Building a keeper that is reliable, cost efficient, and up to date to execute orders across all markets in a timely manner is hard. We provide a keeper that you can run, fork, or use as documentation to build one of your own.
It's recommended to use off-chain delayed orders as the preferred method to open positions. It has the lowest fees and provides the fastest execution, giving traders the best UX.

How to Close a Position

To close a position is the same as modifying an existing position with equal but inverted size. For example to close a 100 ETH long, you simply short -100 ETH. Once a position has been opened, regardless of how (i.e. order type), it can be closed in any method, async or otherwise. The only difference is the execution and fees charged on exit.
const market = new Contract(address, abi, signer);
const position = await market.positions(account);
// Atomically
await market.modifyPosition(-position.size, priceImpactDelta);
// Convenience method for atomic orders.
await market.closePosition(priceImpactDelta);
// Delayed orders
await market.submitDelayedOrder(-position.size, priceImpactDelta);
// Delayed off-chain orders
await market.submitOffchainDelayedOrder(-position.size, priceImpactDelta);
In the coming release, we hope to provide a closeDelayedOrder and closeOffchainDelayedOrder convenience methods, similar to that of closePosition.

What is Price Impact / Fill Price?

Price impact refers to the price change in the market that is directly caused by your trade. In perps, price impact is relative to skew. Skew is the size delta between all open long and short positions, always optimising for a perfectly balanced skew. For example, market that is 80% long and 20% short is skewed 60% long.
If the position’s size increases/expands the market skew, they are charged a premium in the form of a higher fill price but if the skew is decreases/contracts, a discount is applied. This happens automatically when a trade is made and it affects both atomic and delayed orders.
If you're building a frontend to quote trader's entry/exit price, it's important to display this fill price. We provide a fillPrice view to help with this:
function fillPrice(int sizeDelta) external view returns (uint price, bool invalid);
This topic is extensively covered in SIP-279, example simulation code, and intrinsically within the smart contract codebase.

Price Impact Delta (units)

priceImpactDelta argument passed to open/close methods are percentages between 0 and 1. For clarity below are some examples:
Basis Points
The specified priceImpactDelta provides trade price protection, particularly for delayed orders. Price may move and many orders/trades may be executed between submission and execution. Providing an acceptable delta prevents the trade from proceeding if the price moves too much.
priceImpactDelta is then used to derive the trade's upper/lower acceptable price limit and reverting if above or below (depending on long or short respectively).
priceImpactLimit (long) = price * (1 + priceImpactDelta)
priceImpactLimit (short) = price * (1 - priceImpactDelta)

What are Funding Rates?

Funding rates on Synthetix perps is one mechanism that encourages a balanced market. A balanced market is one such that the size of longs is equal to size of shorts. A market skew is present when longs do not balance shorts or vice versa. Whenever the market is imbalanced in the sense that there is more open interest on one side, SNX holders take on market risk.
Funding rates floats. It moves up and down proportional to the skew with a funding rate velocity that dictates how quickly it can move in either direction. In practice, the effect of this is that funding rates will continuously drift higher/lower in the presence of uncorrected position imbalances, creating a natural price discovery mechanism for funding rates while simultaneously smoothing out funding rate trajectories.
We provide two methods to retrieve both the current funding rate and current funding velocity.
* The current funding rate as determined by the market skew; this is returned as a percentage per day.
* If this is positive, shorts pay longs, if it is negative, longs pay shorts.
function currentFundingRate() external view returns (int);
* Velocity is a measure of how quickly the funding rate increases or decreases. A positive velocity means
* funding rate is increasing positively (long skew). A negative velocity means the skew is on shorts.
function currentFundingVelocity() external view returns (int);
Funding rates are stored in 24hr periods. To calculate the hourly funding rate:
const fundingRate = await market.currentFundingRate().div(24);

Trading Fees

Traders are charged makerFee if the size of their order reduces the market skew and a takerFee if the order increases the skew. However, in certain situations, an order can decrease and increase the skew. In these scenarios, the proportion of size that decreases the skew should be charged a makerFee and the remaining, a takerFee. Below is a concrete example:
price = 100
makerFee = 0.001
takerFee = 0.003
# Increasing the skew
skew = 10
size = 5
skewResult = 15
fee = 5 * price * takerFee
# Decreasing the skew
skew = 10
size = -3
skewResult = 7
fee = 3 * price * makerFee
# Increasing and decreasing the skew
skew = 10
size = -15
skewResult = -5
fee = 10 * price * makerFee + 5 * price * takerFee
We provide an orderFee view to assist with fee calculation.
function orderFee(int sizeDelta) external view returns (uint fee, bool invalid);
Note that orderFee does not consider the keeper fee paid out for async order executions. A static fee is paid out from a trader's available margin on successful execution. However is fully refunded if the trader is the executor.

Trade Simulations

In certain situations, it's often useful to see the result of a trade without making it. You can simulate a trade for a particular sender (i.e. trader) by invoking the postTradeDetails view.
function postTradeDetails(
int sizeDelta,
uint tradePrice,
address sender
returns (
uint margin,
int size,
uint price,
uint liqPrice,
uint fee,
IPerpsV2MarketBaseTypes.Status status
Note that if tradePrice == 0 then we use the current oracle price.
Looking to migrate an existing integration? Take a look at our migration guides.