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:

  1. Vault: where all users' funds are stored, including USDC and non-USDC collaterals

  2. 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

  3. AccountBalance: where most of the information of a trader can be queried, such as position size, position value, etc


Install Curie npm package

npm install @perp/curie-contract 

Node version: 12 (or 16 for M1 CPU)


Vault

This contract is mainly used for depositing and withdrawing collaterals.

Deposit

Vault.deposit

Deposit collateral

function deposit(address token, uint256 amount) external;

Parameters:

  • token: the address of the collateral

  • amount: the amount to be deposited

Example:

IVault(VAULT_ADDR).deposit(TOKEN_ADDR, AMOUNT)

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

function getFreeCollateral(address trader) external view returns (uint256);

Parameter:

  • trader: the address of the trader


Vault.withdraw

Withdraw collaterals of the specified amount

function withdraw(address token, uint256 amount) external;

Parameters:

  • token: the address of the collateral

  • amount: the amount to be withdrawn, which should not exceed freeCollateral

Example:

IVault vault = IVault(VAULT_ADDR);

uint256 freeCollateral = vault.getFreeCollateral(TRADER_ADDR);

vault.withdraw(TOKEN_ADDR, AMOUNT);

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

struct OpenPositionParams {
    address baseToken;
    bool isBaseToQuote;
    bool isExactInput;
    uint256 amount;
    uint256 oppositeAmountBound;
    uint256 deadline;
    uint160 sqrtPriceLimitX96;
    bytes32 referralCode;
}

function openPosition(OpenPositionParams memory params) external returns (uint256 base, uint256 quote);

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

ClearingHouse clearingHouse = ClearingHouse(CH_ADDR);

IClearingHouse.OpenPositionParams params = IClearingHouse.OpenPositionParams({
    baseToken: VETH_ADDR,
    isBaseToQuote: false, // false for longing
    isExactInput: false, // false for specifying the output vETH amount
    amount: 1 ether,
    oppositeAmountBound: 0, // no amount limit
    sqrtPriceLimitX96: 0 // no price limit
    deadline: block.timestamp + 900, // take 15 minutes for example
    referralCode: 0x0000000000000000000000000000000000000000000000000000000000000000 // no referral code
})

// quote is the amount of quote token the taker pays
// base is the amount of base token the taker gets
(uint256 base, uint256 quote) = clearingHouse.openPosition(params)

Close Position

Close an existing position

ClearingHouse.closePosition

struct ClosePositionParams {
    address baseToken;
    uint160 sqrtPriceLimitX96;
    uint256 oppositeAmountBound;
    uint256 deadline;
    bytes32 referralCode;
}

function closePosition(ClosePositionParams calldata params) external returns (uint256 base, uint256 quote);

The params are pretty much the same as openPosition.

Example:

  • Close the 1 vETH long position in the above example of openPosition

ClearingHouse clearingHouse = ClearingHouse(CH_ADDR);

IClearingHouse.ClosePositionParams params = IClearingHouse.ClosePositionParams({
    baseToken: VETH_ADDR,
    sqrtPriceLimitX96: 0, 
    oppositeAmountBound: 0,
    deadline: block.timestamp + 900,
    referralCode: 0x0000000000000000000000000000000000000000000000000000000000000000 
})

(uint256 base, uint256 quote) = clearingHouse.closePosition(params)

Add Liquidity

ClearingHouse.addLiquidity

Provide liquidity

struct AddLiquidityParams {
    address baseToken;
    uint256 base;
    uint256 quote;
    int24 lowerTick;
    int24 upperTick;
    uint256 minBase;
    uint256 minQuote;
    uint256 deadline;
}

struct AddLiquidityResponse {
    uint256 base;
    uint256 quote;
    uint256 fee;
    uint256 liquidity;
}

function addLiquidity(AddLiquidityParams calldata params) external returns (AddLiquidityResponse memory)

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

ClearingHouse clearingHouse = ClearingHouse(CH_ADDR);

IClearingHouse.AddLiquidityParams params = ClearingHouse.AddLiquidityParams({
    baseToken: VETH_ADDR,
    base: 2 ether,
    quote: 100 ether,
    lowerTick: 50000,
    upperTick: 51000,
    minBase: 0,
    minQuote: 0,
    deadline: block.timestamp
})

IClearingHouse.AddLiquidityResponse memory response = clearingHouse.addLiquidity(params);

Remove Liquidity

ClearingHouse.removeLiquidity

struct RemoveLiquidityParams {
    address baseToken;
    int24 lowerTick;
    int24 upperTick;
    uint128 liquidity;
    uint256 minBase;
    uint256 minQuote;
    uint256 deadline;
}

struct RemoveLiquidityResponse {
    uint256 base;
    uint256 quote;
    uint256 fee;
}

function removeLiquidity(RemoveLiquidityParams calldata params) external returns (RemoveLiquidityResponse memory)

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

ClearingHouse clearingHouse = ClearingHouse(CH_ADDR);

IClearingHouse.RemoveLiquidityParams params = ClearingHouse.RemoveLiquidityParams({
    baseToken: VETH_ADDR,
    lowerTick: 50000,
    upperTick: 51000,
    liquidity: 12,
    minBase: 1 ether,
    minQuote: 0,
    deadline: block.timestamp
})

RemoveLiquidityResponse memory response = clearingHouse.removeLiquidity(params);
  • Collect maker's fees by removing zero liquidity

ClearingHouse clearingHouse = ClearingHouse(CH_ADDR);

IClearingHouse.RemoveLiquidityParams params = ClearingHouse.RemoveLiquidityParams({
    baseToken: VETH_ADDR,
    lowerTick: 50000,
    upperTick: 51000,
    liquidity: 0, // removing 0 liquidity is interpreted as to collect the accumulated swapping fees since last collection
    minBase: 0,
    minQuote: 0,
    deadline: block.timestamp
})

// response.fee is the fees a maker gets
RemoveLiquidityResponse memory response = clearingHouse.addLiquidity(params)

Get Account Value

ClearingHouse.getAccountValue

Get the total worth of one's positions denominated in USDC

function getAccountValue(address trader) public view returns (int256);

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

You can then search the address with the blockchain explorer to see the read/write interface of the contracts

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:

    "graphServerConfigs": [
        {
            "url": "https://api.thegraph.com/subgraphs/name/perpetual-protocol/perpetual-v2-optimism",
            "wsUrl": "wss://api.thegraph.com/subgraphs/name/perpetual-protocol/perpetual-v2-optimism",
            "healthUrl": "https://api.thegraph.com/index-node/graphql",
            "name": "perpetual-protocol/perpetual-v2-optimism"
        }
    ]
  • 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

    "candleServerConfigs": [
        {
            "url": "https://4b3vdz2hdjho7gzuo4wl2jgsoe.appsync-api.ap-southeast-1.amazonaws.com/graphql",
            "region": "ap-southeast-1",
            "key": "xxxxxxxxxx"
        }
    ],
    "statisticsServerConfigs": [
        {
            "url": "https://4b3vdz2hdjho7gzuo4wl2jgsoe.appsync-api.ap-southeast-1.amazonaws.com/graphql",
            "region": "ap-southeast-1",
            "key": "xxxxxxxxxx"
        }
    ]
  • Example

    curl -L -X POST 'https://4b3vdz2hdjho7gzuo4wl2jgsoe.appsync-api.ap-southeast-1.amazonaws.com/graphql' \
         -H 'x-api-key: xxxxxxxxxx' \
         -H 'Content-Type: application/json' \
         -d '{"query":"query MyQuery {\n getStatistics(ammAddr: \"0x8C835DFaA34e2AE61775e80EE29E2c724c6AE2BB\") {\n lastTradePriceUsd\n volume24h\n baseVolume24h\n priceChangeRate24h\n priceHigh24h\n priceLow24h\n }\n}\n","variables":{}}'
  • Candle Service Schema

    #
    # Models
    #
    
    type CandleStick {
        market: String!,
        resolution: String!,
        startTime: Int!,
        open: String!,
        high: String!,
        low: String!,
        close: String!,
        volume: String!,
        baseAssetVol: String!,
        txCount: Int!
        version: Int!
        blockNumber: Int!
    }
    
    #
    # Operations
    #
    
    type Query {
        listCandleSticks(
            query: TableCandleStickQueryInput!, 
            limit: Int, 
            nextToken: String
        ): CandleStickConnection
    }
    
    type Subscription {
        onUpsertCandleStick(
            market: String,
            resolution: String,
            startTime: String
        ): CandleStick
    
            onDeleteCandleStick(
            market: String,
            resolution: String,
            startTime: String
        ): CandleStick
    }
    
    #
    # Operation Models
    #
    
    type CandleStickConnection {
        items: [CandleStick]
            
            # token to get next page data if any
        nextToken: String
    }
    
    input TableCandleStickQueryInput {
            # format: baseToken#resolution
        # ex: 0x7EAdA83e15AcD08d22ad85A1dCE92E5A257Acb92#5m
        marketResolution: TableStringFilterInput
        startTime: TableIntFilterInput
    }
    
    input TableStringFilterInput {
        ne: String
        eq: String
        le: String
        lt: String
        ge: String
        gt: String
        contains: String
        notContains: String
        between: [String]
        beginsWith: String
    }
    
    input TableIntFilterInput {
        ne: Int
        eq: Int
        le: Int
        lt: Int
        ge: Int
        gt: Int
        contains: Int
        notContains: Int
        between: [Int]
    }
  • Statistics Service Schema

    #
    # Models
    #
    
    type Statistics {
        ammAddr: String! # ammAddr is used in place of baseToken in perp v2
        lastTradePriceUsd: String
        volume24h: String
        baseVolume24h: String
        priceChangeRate24h: String
        priceHigh24h: String
        priceLow24h: String
        timestamp: Int!
        blockNumber: Int!
    }
    
    #
    # Operations
    #
    
    type Query {
        getStatistics(ammAddr: String!): Statistics
    }
    
    type Subscription {
        onUpsertStatistics(ammAddr: String): Statistics
    }
    
    #
    # Operation Models
    #
    
    input TableStringFilterInput {
        ne: String
        eq: String
        le: String
        lt: String
        ge: String
        gt: String
        contains: String
        notContains: String
        between: [String]
        beginsWith: String
    }
    
    input TableIntFilterInput {
        ne: Int
        eq: Int
        le: Int
        lt: Int
        ge: Int
        gt: Int
        contains: Int
        notContains: Int
        between: [Int]
    }

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 Graph

    • markPrice from smart contracts

      pool.baseAmount.mul(markPrice).add(pool.quoteAmount)
  • 24h Fees

    • volume24h from AppSync

    • exchangeFeeRatios from smart contracts

    • pool from The Graph

      volume24h.mul(exchangeFeeRatios[pool.baseAddress])
  • Liquidity Pool

Query Trader Info

PendingFundingPayment

// the following is the workaround unless we expose Exchange._getFundingGrowthGlobalAndTwaps()
const lastTraderWhoEmitFundingUpdated
const fundingGrowthGlobal.fundingGrowthGlobal = AccountBalance.getAccountinfo(lastTraderWhoEmitFundingUpdatedFromThatMarket, baseToken).lastTwPremiumGrowthGlobalX96

// now we're fetching the info from the trader we want to calculate the pnl
const traderLastTwPremiumGrowthGlobalX96 = AccountBalance.getAccountinfo(trader, baseToken).lastTwPremiumGrowthGlobalX96
const tradersTakerPosSize = AccountBalance.getTakerPositionSize(trader, baseToken)

// repeat the math of Funding.calcPendingFundingPaymentWithLiquidityCoefficient
const takerPendingFundingPaymentByMarket = tradersTakerPosSize * ((fundingGrowthGlobal.twPremiumX96 - traderLastTwPremiumGrowthGlobalX96) / 2^96 ) / 15mins
const makerFundingPayment = Exchange.getPendingFundingPayment(trader, baseToken) - takerPendingFundingPaymentByMarket
  • Exchange.getPendingFundingPayment(trader, baseToken) is negative if receiving funding payment

Taker or Maker's UnrealizedPnl by Market

const takerPositionSize = AccountBalance.getTakerPositionSize(trader, baseToken)
const makerImpermanentPositionSize = AccountBalance.getTotalPositionSize(trader, baseToken) - takerPositionSize

const takerOpenNotional = AccountBalance.getTakerOpenNotional(trader, baseToken) 
const makerOpenNotional = AccountBalance.getTotalOpenNotional(trader, baseToken) - takerOpenNotional

const takerUnrealizedPnl = takerPositionSize * indexPrice + takerOpenNotional
const makerUnrealizedPnl = makerImpermanentPositionSize * indexPrice + makerOpenNotional

Realized PnL

  1. by event

    1. 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)

    2. PositionChanged + FundingPaymentSettled + PositionLiquidated + RealizedMakerFee

      1. PositionChanged.realizedPnl + FundingPaymentSettled.fundingPayment + PositionLiquidated.liquidationFee

  2. by contract

    1. every time a contract call (ex. openPosition ), store owedRealizedPnl before and after the openPosition by calling AccountBalance.getPnlAndPendingFee

Margin Ratio

There are 2 ways to do it:

  1. Use our npm package @perp/sdk-curie and call Positions' getAccountMarginRatio function. Check code snippet.

  2. Get data from contracts and calculate them. The formula will be the same as in our @perp/sdk-curie.

const accountValue = await Vault.getAccountValue(trader)
const totalAbsPositionValue = await AccountBalance.getTotalAbsPositionValue(trader)
const marginRatio = accountValue / totalAbsPositionValue

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 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:

  1. get IDs of all orders of a maker: OrderBook.getOpenOrderIds(trader, baseToken)

  2. 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:

struct Info {
    uint128 liquidity;
    int24 lowerTick;
    int24 upperTick;
    uint256 lastFeeGrowthInsideX128;
    int256 lastTwPremiumGrowthInsideX96;
    int256 lastTwPremiumGrowthBelowX96;
    int256 lastTwPremiumDivBySqrtPriceGrowthInsideX96;
    uint256 baseDebt;
    uint256 quoteDebt;
}

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

LiquidityAmounts.getAmount0ForLiquidity(
    sqrtMarkPriceX96 > sqrtPriceAtLowerTick ? sqrtMarkPriceX96 : sqrtPriceAtLowerTick,
    sqrtPriceAtUpperTick,
    liquidity
)
  • quoteToken

LiquidityAmounts.getAmount1ForLiquidity(
    sqrtPriceAtLowerTick
    sqrtMarkPriceX96 < sqrtPriceAtUpperTick ? sqrtMarkPriceX96 : sqrtPriceAtUpperTick,
    liquidity
)

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

  1. the current amounts of two tokens

  2. 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:

interface IRewardDelegate {
    // should be called by the partner/trader contract, to delegate to an EOA who locks vePERP on the partner/trader contract's behalf
    function setBeneficiaryCandidate(address candidate) external;

    // should be called by the EOA (beneficiary), to confirm the delegation
    function updateBeneficiary(address truster) external;

    // should be called by the partner/trader contract, if you decided to revoke the delegation
    function clearBeneficiary(address beneficiary) external;

    function getBeneficiaryCandidate(address truster) external view returns (address);

    function getBeneficiaryAndQualifiedMultiplier(address user) external view returns (address, uint256);
}

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():

interface IRewardDelegate {
    function setBeneficiaryCandidate(address candidate) external;
    function clearBeneficiary(address beneficiary) external;
}

// do the same thing for TraderContract
contract PartnerContract is OwnableUpgradeable {
    ...

    // add the following functions to your contract
    function setBeneficiaryCandidate(address candidate) external onlyOwner {
        IRewardDelegate(rewardDelegate).setBeneficiaryCandidate(candidate);
    }

    function clearBeneficiary(address beneficiary) external onlyOwner {
        IRewardDelegate(rewardDelegate).clearBeneficiary(beneficiary);
    }
}

After the deployment, both PartnerContract and TraderContract could then assign Alice as the beneficiary, and Alice needs to confirm 2 delegations:

// for PartnerContract
const partnerSetBeneficiaryCandidateTx = await partnerContract
  .connect(partnerContractOwner)
  .setBeneficiaryCandidate(alice.address)
await partnerSetBeneficiaryCandidateTx.wait()

const aliceUpdateBeneficiaryForPartnerTx = vePERPReferralRewardDelegate
  .connect(alice)
  .updateBeneficiary(partnerContract.address)
await aliceUpdateBeneficiaryForPartnerTx.wait()

// for TraderContract
const traderSetBeneficiaryCandidateTx = await traderContract
  .connect(traderContractOwner)
  .setBeneficiaryCandidate(alice.address)
await traderSetBeneficiaryCandidateTx.wait()

const aliceUpdateBeneficiaryForTraderTx = vePERPReferralRewardDelegate
  .connect(alice)
  .updateBeneficiary(traderContract.address)
await aliceUpdateBeneficiaryForTraderTx.wait()

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:

await vePERPReferralRewardDelegate.getBeneficiaryAndQualifiedMultiplier(partnerContract.address)
// returns [aliceAddress, 3]

await vePERPReferralRewardDelegate.getBeneficiaryAndQualifiedMultiplier(traderContract.address)
// returns [aliceAddress, 3]

await vePERPReferralRewardDelegate.getBeneficiaryAndQualifiedMultiplier(bob.address)
// since no one delegates to Bob, so Bob's beneficiary is himself and his qualifiedMultiplier is 1
// returns [bobAddress, 1]

Last updated