Home

Awesome

GMX Synthetics

Contracts for GMX Synthetics.

General Overview

This section provides a general overview of how the system works.

For a Technical Overview, please see the section further below.

Markets

Markets support both spot and perp trading, they are created by specifying a long collateral token, short collateral token and index token.

Examples:

Liquidity providers can deposit either the long or short collateral token or both to mint liquidity tokens.

The long collateral token is used to back long positions, while the short collateral token is used to back short positions.

Liquidity providers take on the profits and losses of traders for the market that they provide liquidity for.

Having separate markets allows for risk isolation, liquidity providers are only exposed to the markets that they deposit into, this potentially allow for permissionless listings.

Traders can use either the long or short token as collateral for the market.

Features

The contracts support the following main features:

Oracle System

To avoid front-running issues, most actions require two steps to execute:

Prices are provided by an off-chain oracle system, which continually signs prices based on the time the prices were queried.

Both a minimum price and a maximum price is signed, this allows information about bid-ask spreads to be included.

Prices stored within the Oracle contract represent the price of one unit of the token using a value with 30 decimals of precision.

Representing the prices in this way allows for conversions between token amounts and fiat values to be simplified, e.g. to calculate the fiat value of a given number of tokens the calculation would just be: token amount * oracle price, to calculate the token amount for a fiat value it would be: fiat value / oracle price.

Fees and Pricing

Funding fees and price impact keep longs / shorts balanced while reducing the risk of price manipulation.

Keepers

There are a few keepers and nodes in the system:

Structure

There are a few main types of contracts:

The contracts are separated into these types to allow for gradual upgradeability.

Majority of data is stored using the DataStore contract.

*storeUtils contracts store struct data using the DataStore, this allows new keys to be added to structs.

EnumberableSets are used to allow order lists and position lists to be easily queried by interfaces or keepers, this is used over indexers as there may be a lag for indexers to sync the latest block. Having the lists stored directly in the contract also helps to ensure that accurate data can be retrieved and verified when needed.

*eventUtils contracts emit events using the event emitter, the events are generalized to allow new key-values to be added to events without requiring an update of ABIs.

GLV

Short for GMX Liquidity Vault: a wrapper of multiple markets with the same long and short tokens. Liquidity is automatically rebalanced between underlying markets based on markets utilisation.

Technical Overview

This section provides a technical description of the contracts.

Exchange Contracts

Markets

Markets are created using MarketFactory.createMarket, this creates a MarketToken and stores a Market.Props struct in the MarketStore.

The MarketToken is used to keep track of liquidity providers share of the market pool and to store the tokens for each market.

At any point in time, the price of a MarketToken is (worth of market pool) / MarketToken.totalSupply(), the function MarketUtils.getMarketTokenPrice can be used to retrieve this value.

The worth of the market pool is the sum of

Deposits

Deposits add long / short tokens to the market's pool and mints MarketTokens to the depositor.

Requests for deposits are created by calling ExchangeRouter.createDeposit, specifying:

Deposit requests are executed using DepositHandler.executeDeposit, if the deposit was created at timestamp n, it should be executed with the oracle prices after timestamp n.

The amount of MarketTokens to be minted, before fees and price impact, is calculated as (worth of tokens deposited) / (worth of market pool) * MarketToken.totalSupply().

Withdrawals

Withdrawals burn MarketTokens in exchange for the long / short tokens of a market's pool.

Requests for withdrawals are created by calling ExchangeRouter.createWithdrawal, specifying:

Withdrawal requests are executed using WithdrawalHandler.executeWithdrawal, if the withdrawal was created at timestamp n, it should be executed with the oracle prices after timestamp n.

The amount of long or short tokens to be redeemed, before fees and price impact, is calculated as (worth of market tokens) / (long / short token price).

Market Swaps

Long and short tokens of a market can be swapped for each other.

For example, if the ETH / USD market has WETH as the long token and USDC as the short token, WETH can be sent to the market to be swapped for USDC and USDC can be sent to the market to be swapped for WETH.

Swap order requests are created by calling ExchangeRouter.createOrder, specifying:

The swap output amount, before fees and price impact, (amount of tokens in) * (token in price) / (token out price).

Market swap order requests are executed using OrderHandler.executeOrder, if the order was created at timestamp n, it should be executed with the oracle prices after timestamp n.

Limit Swaps

Passive swap orders that should be executed when the output amount matches the minimum output amount specified by the user.

Limit swap order requests are executed using OrderHandler.executeOrder, if the order was created at timestamp n, it should be executed with oracle prices after timestamp n.

Market Increase

Open or increase a long / short perp position.

Market increase order requests are created by calling ExchangeRouter.createOrder, specifying:

Market increase order requests are executed using OrderHandler.executeOrder, if the order was created at timestamp n, it should be executed with the oracle prices after timestamp n.

Limit Increase

Passive increase position orders that should be executed when the index token price matches the acceptable price specified by the user.

Long position example: if the current index token price is $5000, a limit increase order can be created with acceptable price as $4990, the order can be executed when the index token price is <= $4990.

Short position example: if the current index token price is $5000, a limit increase order can be created with acceptable price as $5010, the order can be executed when the index token price is >= $5010.

Limit increase order requests are executed using OrderHandler.executeOrder, if the order was created at timestamp n, it should be executed with the oracle prices after timestamp n.

Market Decrease

Close or decrease a long / short perp position.

Market decrease order requests are created by calling ExchangeRouter.createOrder, specifying:

Market decrease order requests are executed using OrderHandler.executeOrder, if the order was created at timestamp n, it should be executed with the oracle prices after timestamp n.

Limit Decrease

Passive decrease position orders that should be executed when the index token price matches the acceptable price specified by the user.

Long position example: if the current index token price is $5000, a limit decrease order can be created with acceptable price as $5010, the order can be executed when the index token price is >= $5010.

Short position example: if the current index token price is $5000, a limit decrease order can be created with acceptable price as $4990, the order can be executed when the index token price is <= $4990.

Limit decrease order requests are executed using OrderHandler.executeOrder, if the order was created at timestamp n, it should be executed with the oracle prices after timestamp n.

Stop-Loss Decrease

Passive decrease position orders that should be executed when the index token price crosses the acceptable price specified by the user.

Long position example: if the current index token price is $5000, a stop-loss decrease order can be created with acceptable price as $4990, the order can be executed when the index token price is <= $4990.

Short position example: if the current index token price is $5000, a stop-loss decrease order can be created with acceptable price as $5010, the order can be executed when the index token price is >= $5010.

Stop-loss decrease order requests are executed using OrderHandler.executeOrder, if the order was created at timestamp n, it should be executed with the oracle prices after timestamp n.

Example 1

The price of ETH is 5000, and ETH has 18 decimals.

The price of one unit of ETH is 5000 / (10 ^ 18), 5 * (10 ^ -15).

To handle the decimals, multiply the value by (10 ^ 30).

Price would be stored as 5000 / (10 ^ 18) * (10 ^ 30) => 5000 * (10 ^ 12).

For gas optimization, these prices are sent to the oracle in the form of a uint8 decimal multiplier value and uint32 price value.

If the decimal multiplier value is set to 8, the uint32 value would be 5000 * (10 ^ 12) / (10 ^ 8) => 5000 * (10 ^ 4).

With this config, ETH prices can have a maximum value of (2 ^ 32) / (10 ^ 4) => 4,294,967,296 / (10 ^ 4) => 429,496.7296 with 4 decimals of precision.

Example 2

The price of BTC is 60,000, and BTC has 8 decimals.

The price of one unit of BTC is 60,000 / (10 ^ 8), 6 * (10 ^ -4).

Price would be stored as 60,000 / (10 ^ 8) * (10 ^ 30) => 6 * (10 ^ 26) => 60,000 * (10 ^ 22).

BTC prices maximum value: (2 ^ 64) / (10 ^ 2) => 4,294,967,296 / (10 ^ 2) => 42,949,672.96.

Decimals of precision: 2.

Example 3

The price of USDC is 1, and USDC has 6 decimals.

The price of one unit of USDC is 1 / (10 ^ 6), 1 * (10 ^ -6).

Price would be stored as 1 / (10 ^ 6) * (10 ^ 30) => 1 * (10 ^ 24).

USDC prices maximum value: (2 ^ 64) / (10 ^ 6) => 4,294,967,296 / (10 ^ 6) => 4294.967296.

Decimals of precision: 6.

Example 4

The price of DG is 0.00000001, and DG has 18 decimals.

The price of one unit of DG is 0.00000001 / (10 ^ 18), 1 * (10 ^ -26).

Price would be stored as 1 * (10 ^ -26) * (10 ^ 30) => 1 * (10 ^ 3).

DG prices maximum value: (2 ^ 64) / (10 ^ 11) => 4,294,967,296 / (10 ^ 11) => 0.04294967296.

Decimals of precision: 11.

Decimal Multiplier

The formula to calculate what the decimal multiplier value should be set to:

Decimals: 30 - (token decimals) - (number of decimals desired for precision)

For Data Stream Feeds

Example calculation for WNT:

Example calculation for WBTC:

The formula for the multiplier is: 10 ^ (60 - dataStreamDecimals - tokenDecimals)

Funding Fees

Funding fees incentivise the balancing of long and short positions, the side with the larger open interest pays a funding fee to the side with the smaller open interest.

Funding fees for the larger side is calculated as (funding factor per second) * (open interest imbalance) ^ (funding exponent factor) / (total open interest).

For example if the funding factor per second is 1 / 50,000, and the funding exponent factor is 1, and the long open interest is $150,000 and the short open interest is $50,000 then the funding fee per second for longs would be (1 / 50,000) * 100,000 / 200,000 => 0.00001 => 0.001%.

The funding fee per second for shorts would be -0.00001 * 150,000 / 50,000 => 0.00003 => -0.003%.

It is also possible to set a fundingIncreaseFactorPerSecond value, this would result in the following funding logic:

Examples

Example 1

Since longShortImbalance > thresholdForStableFunding, savedFundingFactorPerSecond should increase by 0.0001% * 6% * 600 = 0.0036%

Example 2

Since longs are already paying shorts, the skew is the same, and the longShortImbalance < thresholdForStableFunding, savedFundingFactorPerSecond should not change

Example 3

Since longShortImbalance < thresholdForDecreaseFunding, savedFundingFactorPerSecond should decrease by 0.000002% * 600 = 0.0012%

Example 4

Since the skew is in the other direction, savedFundingFactorPerSecond should decrease by 0.0001% * 1% * 600 = 0.0006%

Note that there are possible ways to game the funding fees, the funding factors should be adjusted to minimize this possibility:

Borrowing Fees

There is a borrowing fee paid to liquidity providers, this helps prevent users from opening both long and short positions to take up pool capacity without paying any fees.

Borrowing fees can use a curve model or kink model.

To use the curve model, the keys to configure would be BORROWING_FACTOR and BORROWING_EXPONENT_FACTOR, the borrowing factor per second would be calculated as:

// reservedUsd is the total USD value reserved for positions
reservedUsd = MarketUtils.getReservedUsd(...)

// poolUsd is the USD value of the pool excluding pending trader PnL
poolUsd = MarketUtils.getPoolUsdWithoutPnl(...)

// reservedUsdAfterExponent is the reservedUsd after applying the borrowingExponentFactor for the market

reservedUsdAfterExponent = applyExponentFactor(reservedUsd, borrowingExponentFactor)

borrowingFactorPerSecond = borrowingFactor * reservedUsdAfterExponent / poolUsd

To use the kink model, the keys to configure would be OPTIMAL_USAGE_FACTOR, BASE_BORROWING_FACTOR and ABOVE_OPTIMAL_USAGE_BORROWING_FACTOR, the borrowing factor per second would be calculated as:

// usageFactor is the ratio of value reserved for positions to available value that can be reserved
usageFactor = MarketUtils.getUsageFactor(...)

borrowingFactorPerSecond = baseBorrowingFactor * usageFactor

if (usageFactor > optimalUsageFactor) {
  diff = usageFactor - optimalUsageFactor
  additionalBorrowingFactorPerSecond = aboveOptimalUsageBorrowingFactor - baseBorrowingFactor

  borrowingFactorPerSecond += additionalBorrowingFactorPerSecond * diff / (Precision.FLOAT_PRECISION - optimalUsageFactor)
}

There is also an option to set a skipBorrowingFeeForSmallerSide flag, this would result in the borrowing fee for the smaller side being set to zero. For example, if there are more longs than shorts and skipBorrowingFeeForSmallerSide is true, then the borrowing fee for shorts would be zero.

Price Impact

The code for price impact can be found in the /pricing contracts.

Price impact is calculated as:

(initial USD difference) ^ (price impact exponent) * (price impact factor) - (next USD difference) ^ (price impact exponent) * (price impact factor)

For swaps, imbalance is calculated as the difference in the worth of the long tokens and short tokens.

For example:

For position actions (increase / decrease position), imbalance is calculated as the difference in the long and short open interest.

price impact exponents and price impact factors are configured per market and can differ for spot and position actions.

Note that this calculation is the price impact for a user's trade not the price impact on the pool. For example, a user's trade may have a 0.25% price impact, the next trade for a very small amount may have a 0.5% price impact.

The purpose of the price impact is to:

Since the contracts use an oracle price which would be an average or median price of multiple reference exchanges. Without a price impact, it may be profitable to manipulate the prices on reference exchanges while executing orders on the contracts.

This risk will also be present if the positive and negative price impact values are similar, for that reason the positive price impact should be set to a low value in times of volatility or irregular price movements.

For the price impact on position increases / decreases, if negative price impact is deducted as collateral from the position, this could lead to the position having a different leverage from what the user intended, so instead of deducting collateral the position's entry / exit price is adjusted based on the price impact.

For example:

If the index token is different from both the long and short token of the market, then it is possible that the pool value becomes significantly affected by the position impact pool, if the position impact pool is very large and the index token has a large price increase. An option to gradually reduce the size of the position impact pool may be added if this becomes an issue.

Price impact is also tracked using a virtual inventory value for positions and swaps, this tracks the imbalance of tokens across similar markets, e.g. ETH/USDC, ETH/USDT.

In case of a large price movement, it is possible that a large amount of positions are decreased or liquidated on one side causing a significant imbalance between long and short open interest, this could lead to very high price impact values. To mitigate this, a max position impact factor value can be configured. If the current price impact exceeds the max negative price impact, then any excess collateral deducted beyond the max negative price impact would be held within the contract, if there was no price manipulation detected, this collateral can be released to the user. When the negative price impact is capped, it may be profitable to open and immediately close positions, since the positive price impact may now be more than the capped negative price impact. To avoid this, the max positive price impact should be configured to be below the max negative price impact.

Fees

There are configurable swap fees and position fees and per market.

Execution fees are also estimated and accounted for on creation of deposit, withdrawal, order requests so that keepers can execute transactions at a close to net zero cost.

Reserve Amounts

If a market has stablecoins as the short collateral token it should be able to fully pay short profits if the max short open interest does not exceed the amount of stablecoins in the pool.

If a market has a long collateral token that is different from the index token, the long profits may not be fully paid out if the price increase of the index token exceeds the price increase of the long collateral token.

Markets have a reserve factor that allows open interest to be capped to a percentage of the pool size, this reduces the impact of profits of short positions and reduces the risk that long positions cannot be fully paid out.

Market Token Price

The price of a market token depends on the worth of the assets in the pool, and the net pending PnL of traders' open positions.

It is possible for the pending PnL to be capped, the factors used to calculate the market token price can differ depending on the activity:

These different factors can be configured to help liquidity providers manage risk and to incentivise deposits when needed, e.g. capping of trader PnL helps cap the amount the market token price can be decreased by due to trader PnL, capping of PnL for deposits and withdrawals can lead to a lower market token price for deposits compared to withdrawals which can incentivise deposits when pending PnL is high.

Parameters

Roles

Roles are managed in the RoleStore, the RoleAdmin has access to grant and revoke any role.

The RoleAdmin will be the deployer initially, but this should be removed after roles have been setup.

After the initial setup:

Known Issues

Tokens

Keepers

Price Impact

Market Token Price

Virtual Inventory

Blockchain

GLV

Other

Deployment Notes

Upgrade Notes

Integration Notes

Deposit Notes

Withdrawal Notes

Order Notes

Commands

To compile contracts:

npx hardhat compile

To run all tests:

npx hardhat test

export NODE_OPTIONS=--max_old_space_size=4096 may be needed to run tests.

To print code metrics:

npx ts-node metrics.ts

To print test coverage:

npx hardhat coverage