The Position Lifecycle
Creating a Position
To create a Position, first one must issue a PositionRequest in a transaction by hitting the VolPerp contract endpoint create_position_request, which you will note has accounts and data:
#[derive(Accounts)]
#[instruction(data: PositionRequestData)]
pub struct CreatePositionRequest<'info> {
/// The user's token account that will pay for the position request
#[account(mut, associated_token::mint = mint, associated_token::authority = owner)]
pub user_token_account: Box<Account<'info, TokenAccount>>,
/// The position request PDA account that will be created as a result of this instruction
#[account(init, seeds = [b"position_request", pool.key().as_ref(), collateral_custody.mint.key().as_ref(), owner.key().as_ref(), data.time_window.as_ref(), data.side.to_le_bytes().as_ref()], payer = payer, space = 8 + PositionRequest::LEN, bump)]
pub position_request: Box<Account<'info, PositionRequest>>,
/// The collateral token account that will be created as a result of this instruction. It will hold the tokens for the position request.
#[account(init, payer = payer, associated_token::mint = mint, associated_token::authority = position_request)]
pub position_request_collateral_token: Box<Account<'info, TokenAccount>>,
pub owner: AccountInfo<'info>,
/// The account that will pay for the transaction fees.
#[account(mut)]
pub payer: Signer<'info>,
/// The pool account that this position request is associated with
#[account(seeds=[b"pool", pool.name.as_bytes().as_ref(), pool.state.key().as_ref(), pool.lp_mint.key().as_ref()], bump)]
pub pool: Box<Account<'info, Pool>>,
/// The custody account that will holds the details on the collateral token
#[account(mut, has_one = pool, has_one = mint, seeds=[b"custody", pool.key().as_ref(), mint.key().as_ref()], bump)]
pub collateral_custody: Box<Account<'info, Custody>>,
/// The oracle account that will be used to get the volatility data
#[account(owner = ORACLE_ID)]
pub oracle: Account<'info, OracleAccount>,
/// The mint account for the collateral token
pub mint: Account<'info, Mint>,
/// The account with authority to transfer tokens from the owner's token account to the position request collateral token account
pub transfer_authority: Signer<'info>,
pub token_program: Program<'info, Token>,
pub associated_token_program: Program<'info, AssociatedToken>,
pub system_program: Program<'info, System>,
pub rent: Sysvar<'info, Rent>,
pub clock: Sysvar<'info, Clock>,
}
// Note there is data for this request.
#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
pub struct PositionRequestData {
/// The USD amount to increase or decrease the position size by.
/// The amount is an integer in the atomic value (before decimals which is 6 for USDC / UST mints).
///
/// This is including decimals. So like 1_000_000 is 1 dollar
pub size_usd_delta: u64,
/// For opening positions and collateral deposits, collateral_delta is the token amount to increase or decrease the position
/// collateral size by. The token amount is represented in atomic values (before decimals).
pub collateral_delta: u64,
pub request_change: u8, // Unused
/// request_change will be equal to Increase (0) for open position and collateral deposit requests,
/// and Decrease (1) for close/partial close position and collateral withdrawal requests.
pub request_type: RequestPositionType,
/// Long for long positions, Short for short positions
pub side: Side, // 0 for Long, 1 for Short
/// The maximum bps slippage for position requests when opening, closing, or updating the position size.
pub vol_slippage: u32,
/// For requests that require token swaps, the output amount of the token swap must be greater than or equal to
/// imperial_minimum_out, else the request will fail.
pub imperial_minimum_out: u64,
/// Legacy value from Jup... currently not used.
///
/// Jup docs: This is an internal attribute used by the program to calculate the collateral_delta for position requests that require token swaps.
pub pre_swap_amount: u64,
/// Legacy value from Jup... currently not used.
///
/// Jup docs: The price (USD) used for TP / SL position requests.
pub trigger_price: u64,
/// Legacy value from Jup... currently not used.
///
/// Jup docs: When trigger_above_threshold is true, the TP / SL position request will
/// be triggered when the position's token price is greater than or equal to trigger_price.
/// When trigger_above_threshold is false, the TP / SL position request will be triggered when the
/// position's token price is less than or equal to trigger+price.
pub trigger_above_threshold: bool,
/// This attribute is only checked when closing or decreasing position sizes.
/// When decrease_type is EntirePosition, the entire position will be closed (i.e. a close position request).
/// When decrease_type is CollateralOnly, the position will be closed only by collateral_delta (i.e. a collateral withdrawal request).
/// When decrease_type is PartialProportional, the position will be closed proportionally to the collateral_delta (i.e. a partial position close request).
/// When decrease_type is None, the position size will be increased by collateral_usd_delta and size_usd_delta..
pub decrease_type: DecreasePositionType,
/// Legacy value from Jup... currently not used.
///
/// The random integer seed used to derive the position request address.
pub counter: u64,
/// The time window used to calculate the implied volatility and realized volatility difference.
///
/// Valid values are:
/// * "1d" - 1 day
/// * "1w" - 1 week
/// * "2w" - 2 weeks
/// * "30d" - 30 days
/// * "60d" - 60 days
/// * "90d" - 90 days
/// * "120d" - 120 days
/// * "180d" - 180 days
pub time_window: String, // TODO: Change to enum
/// The public key of the trader's account.
pub owner: Pubkey,
/// The amount of lamports to transfer to the position request account.
pub lamports: u64,
}
impl Default for PositionRequestData {
fn default() -> Self {
Self {
size_usd_delta: 0,
collateral_delta: 0,
request_change: 0,
request_type: RequestPositionType::Increase,
side: Side::Long as u8,
vol_slippage: 0,
imperial_minimum_out: 0,
pre_swap_amount: 0,
trigger_price: 0,
trigger_above_threshold: false,
decrease_type: DecreasePositionType::None,
counter: 0,
time_window: "5m".to_string(),
owner: Pubkey::default(),
lamports: 0,
}
}
}
Once this is complete, you'll temporarily have a PositionRequest, and you'll need to wait for a follow up tx to be completed by the Rust crank backend with admin access where your Position will be created.
We aim for this Rust crank to have a maximum turn-around time of 1-2 minutes.
The instruction being run in this tx is the update_position endpoint on VolPerp which is idempotent and requires no data. You'll see it show up within a few minutes if you follow your own PositionRequest in any Solana Explorer:
#[derive(Accounts)]
pub struct UpdatePosition<'info> {
#[account(seeds=[b"state"], bump)]
pub state: Box<Account<'info, State>>,
#[account(mut)]
pub payer: Signer<'info>,
#[account(mut)]
/// CHECK: this is checked by checking the key on position
pub position_payer: AccountInfo<'info>,
#[account(mut)]
/// CHECK: This is checked on position_request has one
pub request_payer: AccountInfo<'info>,
/// CHECK: The owner is checked on position_request has one
#[account(mut)]
pub owner: AccountInfo<'info>,
#[account(mut, has_one=state)]
pub pool: Box<Account<'info, Pool>>,
#[account(mut, seeds=[b"custody", pool.key().as_ref(), collateral_custody.mint.key().as_ref()],bump,has_one=pool)]
pub collateral_custody: Box<Account<'info, Custody>>,
#[account(mut, associated_token::mint = collateral_custody.mint, associated_token::authority = state)]
pub protocol_token_account: Box<Account<'info, TokenAccount>>,
#[account(mut, associated_token::mint = collateral_custody.mint, associated_token::authority = collateral_custody)]
pub custody_token_account: Box<Account<'info, TokenAccount>>,
#[account(mut, seeds = [b"insurance", collateral_custody.key().as_ref(), collateral_custody.mint.as_ref()], bump, token::mint = collateral_custody.mint, token::authority = collateral_custody)]
pub insurance_token_account: Box<Account<'info, TokenAccount>>,
#[account(mut, seeds=[b"position_request", pool.key().as_ref(), collateral_custody.mint.key().as_ref(), position_request.owner.as_ref(), position_request.time_window.as_ref(), position_request.side.to_le_bytes().as_ref()], bump, has_one = owner, has_one=pool, has_one=request_payer, has_one=collateral_custody)]
pub position_request: Box<Account<'info, PositionRequest>>,
#[account(mut, associated_token::mint = collateral_custody.mint, associated_token::authority = position_request.owner)]
pub user_token_account: Box<Account<'info, TokenAccount>>,
#[account(mut, associated_token::mint = collateral_custody.mint, associated_token::authority = position_request)]
pub position_request_collateral_token: Box<Account<'info, TokenAccount>>,
#[account(init_if_needed, payer = payer, seeds = [
b"position",
pool.key().as_ref(),
collateral_custody.mint.as_ref(),
position_request.owner.as_ref(),
position_request.time_window.as_ref(),
position_request.side.to_le_bytes().as_ref()
], bump, space = 8 + Position::LEN)]
pub position: Box<Account<'info, Position>>,
#[account(owner = ORACLE_ID)]
pub oracle: Box<Account<'info, OracleAccount>>,
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
pub clock: Sysvar<'info, Clock>,
}
Notably, the lamports from the PositionRequest will be returned to the original request_payer once the PositionRequest is destroyed after this second transaction. When your Position closes, the lamports from that are also returned to the original request_payer.
What's Next?
Let's work through some examples of what you can do once you have an open Position. Your Position pubkey is easily derived from the seeds:
[
b"position",
pool.key().as_ref(),
collateral_custody.mint.as_ref(),
position_request.owner.as_ref(),
position_request.time_window.as_ref(),
position_request.side.to_le_bytes().as_ref()
]
So it can be easy to monitor it and work through various strategies.
Increasing Position
To issue an update to this Position, like to increase the collateral under management, simply follow the same steps above, but replace values for decrease_type and request_type in the PositionRequestData with DecreasePositionType::None, and RequestPositionType::Increase, and collateral_delta set to the value you desire. The system will collect your collateral and add it.
If you wish to add only collateral, size_delta should be equal to collateral_delta, whereas if you wish to add more leverage, you should have size_delta equal to 2x your collateral_delta if, for example, you want to take on a loan equal in size to your collateral and add both to your current position.
Withdrawing Collateral
You might want to decrease collateral while keeping your loaned amount the same. You can do this by setting your decrease_type to DecreasePositionType::CollateralOnly, setting request_type to RequestPositionType::Decrease, and then setting size_delta and collateral_delta to the value of collateral you wish to remove. It will only cut down on collateral. Be careful, though, this request will increase your leverage, and may fail if it puts you into a liquidation zone!
Partial Closure
If you want to close 50% of your Position, you can by setting decrease_type to DecreasePositionType::PartialProportional, request_type to RequestPositionType::Decrease, and then setting collateral_delta in your PositionRequestData to 50% of the value of your current collateral_usd in Position. When the crank runs, the Position will be decreased during the update_position phase to one half it's collateral_usd and one half it's size_usd value, with the removed collateral and profits being returned to the owner of the Position, fees being paid, and if there are losses instead of profits, those losses being granted to the LPs of the Pool.
Closure
Similar to a Partial Closure, use DecreasePositionType::EntirePosition instead, and the entire Position is closed out after the crank runs, with lamports returned to the original request_payer (which may not be the same as the Position's owner.) The Position's owner will receive all due collateral and profits.
Liquidation?
Liquidation can happen to any position that becomes too profitable or too over-leveraged.
Last updated