Integration guide
Last updated
Was this helpful?
Last updated
Was this helpful?
Content Out of Date
This content is not maintained and refers to an out-of-date version of Perpetual Protocol.
For the latest documentation, see
There are three main contracts:
Vault
: where all users' funds are stored, including USDC and non-USDC collaterals
ClearingHouse
:
the main component that manages all markets of Perp v2
As a taker, one can open or close positions
As a maker, one can add or remove liquidity
As a liquidator, one can liquidate someone's position that is close to or already bankrupt and get liquidation fees as the reward
AccountBalance
: where most of the information of a trader can be queried, such as position size, position value, etc
Node version: 12 (or 16 for M1 CPU)
This contract is mainly used for depositing and withdrawing collaterals.
Deposit collateral
Parameters:
token
: the address of the collateral
amount
: the amount to be deposited
Example:
When withdrawing collaterals, one can withdraw the amount up to one's freeCollateral
. This ensures that one's positions are always sufficiently collateralized.
How many collaterals a trader can withdraw
Parameter:
trader
: the address of the trader
Withdraw collaterals of the specified amount
Parameters:
token
: the address of the collateral
amount
: the amount to be withdrawn, which should not exceed freeCollateral
Example:
ClearingHouse manages all markets of Perp v2.
For each market, we deploy a pair of two virtual tokens (with no real value) and initiate a new Uniswap V3 pool to provide liquidity to.
Base token: the virtual underlying asset users are trading for, such as vETH, vBTC
Quote token: the counter currency of base token, which is always vUSDC for any base token
Open a new position or adjust the position size of an existing one
Parameters:
baseToken
: the address of the base token, which suggests the market to trade in
isBaseToQuote
: true
for shorting the base token asset and false
for longing
isExactInput
: for specifying exactInput
or exactOutput
; similar to UniSwap V2's specs
amount
: the amount specified. Depending on the isExactInput
parameter, this can be either the input amount or output amount.
oppositeAmountBound
: the restriction on how many token to receive/pay, depending on isBaseToQuote
& isExactInput
isBaseToQuote
&& isExactInput
: want more output quote as possible, so we set a lower bound of output quote
isBaseToQuote
&& !isExactInput
: want less input base as possible, so we set a upper bound of input base
!isBaseToQuote
&& isExactInput
: want more output base as possible, so we set a lower bound of output base
!isBaseToQuote
&& !isExactInput
: want less input quote as possible, so we set a upper bound of input quote
deadline
: the restriction on when the tx should be executed; otherwise, tx will get reverted
sqrtPriceLimitX96
: the restriction on the ending price after the swap; 0
for no limit. This is the same as sqrtPriceLimitX96
in the UniSwap V3 contract.
referralCode
: the referral code for partners
Return values:
base
: the amount of base token exchanged
quote
: the amount of quote token exchanged
Example:
Long 1 vETH
Close an existing position
The params are pretty much the same as openPosition
.
Example:
Close the 1 vETH long position in the above example of openPosition
Provide liquidity
Parameters:
baseToken
: the base token address
base
: the amount of base token you want to provide
quote
: the amount of quote token you want to provide
lowerTick
: lower tick of liquidity range, same as UniSwap V3
upperTick
: upper tick of liquidity range, same as UniSwap V3
minBase
: the minimum amount of base token you'd like to provide
minQuote
: the minimum amount of quote token you'd like to provide
deadline
: a time after which the transaction can no longer be executed
Return values:
base
: the amount of base token added to the pool
quote
: the amount of quote token added to the pool
fee
: the amount of fee collected if there is any
liquidity
: the amount of liquidity added to the pool, derived from base
& quote
Example:
Provide liquidity to vETH/vUSDC pair with 2 vETH and 100 vUSDC, in the tick range [50000, 51000)
The range for liquidity on Perp V2 and Uniswap V3 is always expressed in tick
Parameters:
baseToken
: the address of base token
lowerTick
: lower tick of liquidity range, same as UniSwap V3
upperTick
: upper tick of liquidity range, same as UniSwap V3
liquidity
: how much liquidity you want to remove, same as UniSwap V3
minBase
: the minimum amount of base token you want to remove
minQuote
: the minimum amount of quote token you want to remove
deadline
: a time after which the transaction can no longer be executed
Return values:
base
: the amount of base token removed from pool
quote
: the amount of quote token removed from pool
fee
: the amount of fee collected if there is any
Example:
Remove 12 units of liquidity from vETH/vUSDC pair, in the tick range [50000, 51000) with a minimum requirement of 1 ETH that should be successfully removed
Collect maker's fees by removing zero liquidity
Get the total worth of one's positions denominated in USDC
Parameter:
account
: the address of the trader
We see smart contracts as the default data source. For anything that is retrievable via smart contracts directly, we mostly will get them by reading the contracts.
Please find the contract addresses in the metadata
You can then search the address with the blockchain explorer to see the read/write interface of the contracts
NOTE
Use the provided server:
You will see a GraphQL explorer interface by opening the URL.
Service Configs
Example
Candle Service Schema
Statistics Service Schema
Candle Price Chart
Market Statistics
funding rate
volume24h
Change (24h)
Reward
Gas Rebate
Liquidity Mining
Pool APR
TVL
pool
from The Graph
markPrice
from smart contracts
24h Fees
volume24h
from AppSync
exchangeFeeRatios
from smart contracts
pool
from The Graph
Liquidity Pool
Exchange.getPendingFundingPayment(trader, baseToken)
is negative if receiving funding payment
by event
observe PnlRealized
event from AccountBalance
(less preferred, harder to get market)
it will be emitted anytime when addLiquidity, removeLiquidity, openPosition, closePosition, liquidate, cancelExcessOrder and settleAllFunding. It could emit multiple event in 1 action
ex. when liquidating a position, it can
emit PnlRealized first for the funding
emit another one for IF (taking 10% fee as IF’s profit)
realized the position pnl
emit PnlRealized for trader’s liquidation fee (loss)
also emit enother one for the liquidator (profit)
PositionChanged + FundingPaymentSettled
+ PositionLiquidated + RealizedMakerFee
PositionChanged.realizedPnl + FundingPaymentSettled
.fundingPayment + PositionLiquidated.liquidationFee
by contract
every time a contract call (ex. openPosition
), store owedRealizedPnl
before and after the openPosition by calling AccountBalance.getPnlAndPendingFee
There are 2 ways to do it:
Impermanent Loss is used to described unrealized loss of liquidity due to price fluctuation on AMMs, such as Uniswap and Perp V2. As the current price of the pool diverges from the price when the liquidity was first added, the amounts of two tokens in a liquidity range becomes different from the previous states.
For example, let's assume a user Alice adds 1 ETH + 0 USDC
to a range. Later, the price changes and Alice's 1 ETH + 0 USDC
becomes 0 ETH + 90 USDC
. If the current price of ETH is 100 USDC
, this means she has 10
loss, since -1 * 100 (loss from ETH) + 90 (USDC) = -10
.
However, if the current price returns to the original price, the amounts of two tokens will become identical to the original states. This is the reason for the impermanent aspect of the loss, as it is not realized until the liquidity is removed.
On Perp V2, all users, both makers and takers, operate on their positions. Thus, we refer to makers' positions as Impermanent Position due to their constantly changing size (by contrast, takers' position sizes won't change unless takers manually increase, reduce or close their positions.)
The total notional value of an impermanent positions is Impermanent Loss; in other words, Impermanent Loss is induced from Impermanent Positions on Perp V2.
Since only makers have Impermanent Loss, we can focus on the contract OrderBook
, which is responsible for liquidity, or makers' positions, or order as we call in our smart contracts.
address trader
: the address of the maker to query
address baseToken
: the address of the vToken of the market, e.g. vETH or ETH market
int24 lowerTick
: the tick of the lower range of an order
int24 upperTick
: the tick of the upper range of an order
If we don't know the range (lowerTick
& upperTick
) of an order:
get IDs of all orders of a maker: OrderBook.getOpenOrderIds(trader, baseToken)
and get the info of a specific order using its ID: OrderBook.getOpenOrderById(bytes32 orderId)
Else, if we already know the range of an order: OrderBook.getOpenOrder(trader, baseToken, lowerTick, upperTick)
Through the two approaches above, we can get the structure of an order as OpenOrder.Info
:
As Perp V2 is built on Uniswap V3, when querying the amounts of two tokens in an order, we'll be using Uniswap's contract LiquidityAmounts.sol
and TickMath.sol
.
First, translate ticks into square root price (sqrtPrice
) with function TickMath.getSqrtRatioAtTick(tick)
:
uint160 sqrtPriceAtLowerTick = TickMath.getSqrtRatioAtTick(lowerTick)
uint160 sqrtPriceAtUpperTick = TickMath.getSqrtRatioAtTick(upperTick)
Then inputting the two values for sqrtPrice
and liquidity
above, we get from OpenOrder.Info
into LiquidityAmounts.getAmount{0,1}ForLiquidity(sqrtPriceAtLowerTick, sqrtPriceAtUpperTick, liquidity)
baseToken
: LiquidityAmounts.getAmount0ForLiquidity()
quoteToken
: LiquidityAmounts.getAmount1ForLiquidity()
as baseToken
is always token0
and quoteToken
token1
on Perp V2.
However, when the current price of a pool is between sqrtPriceAtLowerTick
and sqrtPriceAtUpperTick
, we have to modify the code above by taking the current price sqrtMarkPriceX96
into consideration:
baseToken
quoteToken
The reason for different parameters in the two scenarios is that baseToken
gets depleted when the price goes up; thus, the first parameter as the lower range has to move accordingly. Similarly, quoteToken
gets depleted when the price goes down and thus the second parameter as the upper price is dependent on the current price.
The suffix X96
in sqrtMarkPriceX96
means the value is scaled by 2^96
as designed by Uniswap V3. It can be fetched by the first return value of UniswapV3Pool.slot0()
.
Notice that in the structure OpenOrder.Info
, there are baseDebt
and quoteDebt
.
The idea of debt is simple: the amount of token a user owes to the exchange.
Thus, baseDebt
is the amount of baseToken
and quoteDebt
, the amount of quoteToken
a user has to pay back when removing liquidity.
This value is registered when an order is initiated by ClearingHouse.addLiquidity()
, e.g. if 1 ETH and 100 USDC are added, baseDebt
will be 1 * 10^18
and quoteDebt
100 * 10^6
, (ETH's decimals are 18 and USDC's decimals are 6).
Now that we have
the current amounts of two tokens
the debt amounts of two tokens
by simply subtracting them, the difference is the Impermanent Position size.
Using the same Alice's example above, let's see what are the balance changes when her 1 ETH + 0 USDC
becomes 0 ETH + 90 USDC
:
quoteToken debt
: 0 vUSD
baseToken debt
: 1 vETH
current quoteToken amount
: 0 vUSD
current baseToken amount
: 1 vETH
Alice's net token amounts:
baseToken
: 1 - 1 = 0
quoteToken
: 0 - 10 = 0
quoteToken debt
: 0 vUSD
baseToken debt
: 1 vETH
current quoteToken amount
: 90 vUSD
current baseToken amount
: 0 vETH
Alice's net token amounts:
baseToken
: 0 - 1 = -1
quoteToken
: 90 - 0 = 90
So we can see that Alice's Impermanent Loss in this scenario is -1 * 100 (loss from ETH) + 90 (USD) = -10
, or -1 * 100 + 90 * 1 = -10
.
The reason is that vUSD
is the settlement token, meaning that vUSD
is always the quoteToken
in any market on Perp V2.
Thus, all baseToken
prices are denominated in vUSD
, as vETH
in this case is 100
, since 1 vETH = 100 vUSD
.
The price of vUSD
, denominated in itself, is of course, 1
.
In the new Referral Program, an account needs to lock 10 vePERP to participate. However, since only EOAs can lock vePERP, if you’re using a contract as a referral partner or trader, your contract must delegate to an EOA (aka the beneficiary) which can lock vePERP on the contract’s behalf. When you claim rewards, the process is the same as before, the only difference is the rewarded vePERP will be distributed to the delegated EOA instead of the contract who owns the referral code or uses a referral code to trade.
No action is required if you’re using an EOA as a referral partner or trader. Only contracts need to delegate.
Here is the interface of vePERPReferralRewardDelegate
:
Assume that you’re using PartnerContract
as a referral partner, the account that owns a referral code; Also, TraderContract
as a referral trader, the account that uses a referral code to trade. And you would like to delegate both PartnerContract
and TraderContract
’s referral rewards to an EOA Alice
.
First, you will need to modify your PartnerContract
and TraderContract
to implement setBeneficiaryCandidate()
:
After the deployment, both PartnerContract
and TraderContract
could then assign Alice
as the beneficiary, and Alice
needs to confirm 2 delegations:
It’s also worth noting that only contracts can call RewardDelegate.setBeneficiaryCandidate()
, and only an EOA can be set as the beneficiary. On the other hand, an EOA cannot set another EOA or contract as the beneficiary. EOAs have no need to delegate since EOAs are not restricted from locking vePERP.
You might notice that there is a getBeneficiaryAndQualifiedMultiplier(user)
function in vePERPReferralRewardDelegate
.
Since contracts cannot lock vePERP, the delegated EOA (beneficiary) is the one who locks vePERP for your contracts. Also, multiple contracts can all delegate to the same beneficiary. We use qualifiedMultiplier * 10 vePERP
to calculate the required vePERP amount of a beneficiary. The base value of qualifiedMultiplier
is 1
when no contracts delegate to the beneficiary; Once the beneficiary accepts any delegation, the qualifiedMultiplier
will be the base value 1
+ how many contracts delegate to the beneficiary.
In the above cases, there are two contracts (PartnerContract
and TraderContract
) that delegate to Alice
as the beneficiary. The qualifiedMultiplier
of Alice
would be 1 (base value) + 2 (two delegations) = 3
. The required vePERP amount of Alice
would be 3 * 10 = 30 vePERP
. That means Alice
must have 30 vePERP, so that the two contracts who delegated to Alice
become eligible to participate in the Referral Program.
The results of calling getBeneficiaryAndQualifiedMultiplier(truster)
would be:
Vault.deposit
Vault.getFreeCollateral
Vault.withdraw
ClearingHouse.openPosition
ClearingHouse.closePosition
ClearingHouse.addLiquidity
ClearingHouse.removeLiquidity
ClearingHouse.getAccountValue
Optimistic:
Optimistic Kovan:
Optimistic:
Optimistic Kovan:
Please check out , a Javascript SDK to interface with our v2 smart contracts.
We use as the default on-chain data indexing service. For any data that cannot be retrieved from smart contracts directly, for example some aggregated data, we mostly will fetch from The Graph.
We use as an alternative to The Graph for time sensitive data, e.g. price chart series, since The Graph can sometimes be out of sync or unstable. We use as little data from AppSync as possible since AppSync is centralized.
Use our npm package and call Positions' getAccountMarginRatio
function. .
Get data from contracts and calculate them. The formula will be the same as in our .
We have written a post for explaining it in more details, see:
Originally
Later when ETH price changes
Your contract can delegate through our vePERPReferralRewardDelegate
contract, and the address is 0x2dE8e18BDaef25C2DE0bED29C8B72E49261CA88d
on Optimism mainnet. You could also check the contract on You could check out the source code on GitHub: .