Integration guide
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 https://docs.perp.com
Which contracts should I be interacting with?
There are three main contracts:
Vault
: where all users' funds are stored, including USDC and non-USDC collateralsClearingHouse
: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
Install Curie npm package
Node version: 12 (or 16 for M1 CPU)
Vault
This contract is mainly used for depositing and withdrawing collaterals.
Deposit
Vault.deposit
Deposit collateral
Parameters:
token
: the address of the collateralamount
: the amount to be deposited
Example:
Withdraw
When withdrawing collaterals, one can withdraw the amount up to one's freeCollateral
. This ensures that one's positions are always sufficiently collateralized.
Vault.getFreeCollateral
How many collaterals a trader can withdraw
Parameter:
trader
: the address of the trader
Vault.withdraw
Withdraw collaterals of the specified amount
Parameters:
token
: the address of the collateralamount
: the amount to be withdrawn, which should not exceedfreeCollateral
Example:
ClearingHouse
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 Position
ClearingHouse.openPosition
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 inisBaseToQuote
:true
for shorting the base token asset andfalse
for longingisExactInput
: for specifyingexactInput
orexactOutput
; similar to UniSwap V2's specsamount
: the amount specified. Depending on theisExactInput
parameter, this can be either the input amount or output amount.oppositeAmountBound
: the restriction on how many token to receive/pay, depending onisBaseToQuote
&isExactInput
isBaseToQuote
&&isExactInput
: want more output quote as possible, so we set a lower bound of output quoteisBaseToQuote
&&!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 revertedsqrtPriceLimitX96
: the restriction on the ending price after the swap;0
for no limit. This is the same assqrtPriceLimitX96
in the UniSwap V3 contract.referralCode
: the referral code for partners
Return values:
base
: the amount of base token exchangedquote
: the amount of quote token exchanged
Example:
Long 1 vETH
Close Position
Close an existing position
ClearingHouse.closePosition
The params are pretty much the same as openPosition
.
Example:
Close the 1 vETH long position in the above example of
openPosition
Add Liquidity
ClearingHouse.addLiquidity
Provide liquidity
Parameters:
baseToken
: the base token addressbase
: the amount of base token you want to providequote
: the amount of quote token you want to providelowerTick
: lower tick of liquidity range, same as UniSwap V3upperTick
: upper tick of liquidity range, same as UniSwap V3minBase
: the minimum amount of base token you'd like to provideminQuote
: the minimum amount of quote token you'd like to providedeadline
: a time after which the transaction can no longer be executed
Return values:
base
: the amount of base token added to the poolquote
: the amount of quote token added to the poolfee
: the amount of fee collected if there is anyliquidity
: the amount of liquidity added to the pool, derived frombase
"e
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
Remove Liquidity
ClearingHouse.removeLiquidity
Parameters:
baseToken
: the address of base tokenlowerTick
: lower tick of liquidity range, same as UniSwap V3upperTick
: upper tick of liquidity range, same as UniSwap V3liquidity
: how much liquidity you want to remove, same as UniSwap V3minBase
: the minimum amount of base token you want to removeminQuote
: the minimum amount of quote token you want to removedeadline
: a time after which the transaction can no longer be executed
Return values:
base
: the amount of base token removed from poolquote
: the amount of quote token removed from poolfee
: 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 Account Value
ClearingHouse.getAccountValue
Get the total worth of one's positions denominated in USDC
Parameter:
account
: the address of the trader
Data Source
Smart Contracts
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.
Exploring Data
Please find the contract addresses in the metadata
Optimistic: https://metadata.perp.exchange/v2/optimism.json
Optimistic Kovan: https://metadata.perp.exchange/v2/optimism-kovan.json
You can then search the address with the blockchain explorer to see the read/write interface of the contracts
Optimistic: https://optimistic.etherscan.io/
Optimistic Kovan: https://kovan-optimistic.etherscan.io/
Usage Examples
NOTE
Please check out https://github.com/perpetual-protocol/sdk-curie, a Javascript SDK to interface with our v2 smart contracts.
The Graph
We use The Graph 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.
Exploring Data
Use the provided server:
You will see a GraphQL explorer interface by opening the URL.
AppSync
We use AppSync 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.
Exploring Data
Please contact the team to get the api-key if needed.
Service Configs
Example
Candle Service Schema
Statistics Service Schema
Usage Examples
Candle Price Chart
Market Statistics
funding rate
volume24h
Change (24h)
Reward
Gas Rebate
Liquidity Mining
Pool APR
Examples of Mixed Data Sources
TVL
pool
from The GraphmarkPrice
from smart contracts
24h Fees
volume24h
from AppSyncexchangeFeeRatios
from smart contractspool
from The Graph
Liquidity Pool
Query Trader Info
PendingFundingPayment
Exchange.getPendingFundingPayment(trader, baseToken)
is negative if receiving funding payment
Taker or Maker's UnrealizedPnl by Market
Realized PnL
by event
observe
PnlRealized
event fromAccountBalance
(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 + RealizedMakerFeePositionChanged.realizedPnl +
FundingPaymentSettled
.fundingPayment + PositionLiquidated.liquidationFee
by contract
every time a contract call (ex.
openPosition
), storeowedRealizedPnl
before and after the openPosition by callingAccountBalance.getPnlAndPendingFee
Margin Ratio
There are 2 ways to do it:
Use our npm package
@perp/sdk-curie
and call Positions'getAccountMarginRatio
function. Check code snippet.Get data from contracts and calculate them. The formula will be the same as in our
@perp/sdk-curie
.
Impermanent Loss Calculation
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.
We have written a post for explaining it in more details, see: What is Impermanent Loss?
Impermanent Position
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.
How to calculate Impermanent Loss 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.
Get Order
address trader
: the address of the maker to queryaddress baseToken
: the address of the vToken of the market, e.g. vETH or ETH marketint24 lowerTick
: the tick of the lower range of an orderint24 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
:
Get Current Amounts of Two Tokens
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()
.
Token Debt
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).
Impermanent Loss Calculation
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
:
Originally
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
Later when ETH price changes
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
.
Referral Program Delegation Guide for Contracts
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.
How to Delegate?
Your contract can delegate through our vePERPReferralRewardDelegate
contract, and the address is 0x2dE8e18BDaef25C2DE0bED29C8B72E49261CA88d
on Optimism mainnet. You could also check the contract on Etherscan. You could check out the source code on GitHub: perpetual-protocol/voting-escrow.
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.
What is QualifiedMultiplier?
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:
Last updated