use {
crate::{
error::PerpetualsError,
math,
state::{
custody::Custody,
oracle::OraclePrice,
perpetuals::Perpetuals,
pool::Pool,
position::{Position, Side},
},
},
anchor_lang::prelude::*,
anchor_spl::token::{Token, TokenAccount},
solana_program::program_error::ProgramError,
};
#[derive(Accounts)]
#[instruction(params: OpenPositionParams)]
pub struct OpenPosition<'info> {
#[account(mut)]
pub owner: Signer<'info>,
#[account(
mut,
constraint = funding_account.mint == custody.mint,
has_one = owner
)]
pub funding_account: Box<Account<'info, TokenAccount>>,
#[account(
seeds = [b"transfer_authority"],
bump = perpetuals.transfer_authority_bump
)]
pub transfer_authority: AccountInfo<'info>,
#[account(
seeds = [b"perpetuals"],
bump = perpetuals.perpetuals_bump
)]
pub perpetuals: Box<Account<'info, Perpetuals>>,
#[account(
mut,
seeds = [b"pool",
pool.name.as_bytes()],
bump = pool.bump
)]
pub pool: Box<Account<'info, Pool>>,
#[account(
init,
payer = owner,
space = Position::LEN,
seeds = [b"position",
owner.key().as_ref(),
pool.key().as_ref(),
custody.key().as_ref(),
&[params.side as u8]],
bump
)]
pub position: Box<Account<'info, Position>>,
#[account(
mut,
seeds = [b"custody",
pool.key().as_ref(),
custody.mint.as_ref()],
bump = custody.bump
)]
pub custody: Box<Account<'info, Custody>>,
#[account(
constraint = custody_oracle_account.key() == custody.oracle.oracle_account
)]
pub custody_oracle_account: AccountInfo<'info>,
#[account(
mut,
seeds = [b"custody_token_account",
pool.key().as_ref(),
custody.mint.as_ref()],
bump = custody.token_account_bump
)]
pub custody_token_account: Box<Account<'info, TokenAccount>>,
system_program: Program<'info, System>,
token_program: Program<'info, Token>,
}
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy)]
pub struct OpenPositionParams {
pub price: u64,
pub collateral: u64,
pub size: u64,
pub side: Side,
}
pub fn open_position(ctx: Context<OpenPosition>, params: &OpenPositionParams) -> Result<()> {
msg!("Check permissions");
let perpetuals = ctx.accounts.perpetuals.as_mut();
let custody = ctx.accounts.custody.as_mut();
require!(
perpetuals.permissions.allow_open_position
&& custody.permissions.allow_open_position
&& !custody.is_stable,
PerpetualsError::InstructionNotAllowed
);
msg!("Validate inputs");
if params.price == 0 || params.collateral == 0 || params.size == 0 || params.side == Side::None
{
return Err(ProgramError::InvalidArgument.into());
}
let position = ctx.accounts.position.as_mut();
let pool = ctx.accounts.pool.as_mut();
let curtime = perpetuals.get_time()?;
let token_price = OraclePrice::new_from_oracle(
custody.oracle.oracle_type,
&ctx.accounts.custody_oracle_account.to_account_info(),
custody.oracle.max_price_error,
custody.oracle.max_price_age_sec,
curtime,
false,
)?;
let token_ema_price = OraclePrice::new_from_oracle(
custody.oracle.oracle_type,
&ctx.accounts.custody_oracle_account.to_account_info(),
custody.oracle.max_price_error,
custody.oracle.max_price_age_sec,
curtime,
custody.pricing.use_ema,
)?;
let min_price = if token_price < token_ema_price {
token_price
} else {
token_ema_price
};
let position_price =
pool.get_entry_price(&token_price, &token_ema_price, params.side, custody)?;
msg!("Entry price: {}", position_price);
if params.side == Side::Long {
require_gte!(
params.price,
position_price,
PerpetualsError::MaxPriceSlippage
);
} else {
require_gte!(
position_price,
params.price,
PerpetualsError::MaxPriceSlippage
);
}
let fee_amount = pool.get_entry_fee(params.size, custody)?;
msg!("Collected fee: {}", fee_amount);
let transfer_amount = math::checked_add(params.collateral, fee_amount)?;
msg!("Amount in: {}", transfer_amount);
msg!("Initialize new position");
let size_usd = min_price.get_asset_amount_usd(params.size, custody.decimals)?;
let collateral_usd = min_price.get_asset_amount_usd(params.collateral, custody.decimals)?;
position.owner = ctx.accounts.owner.key();
position.pool = pool.key();
position.custody = custody.key();
position.open_time = perpetuals.get_time()?;
position.update_time = 0;
position.side = params.side;
position.price = position_price;
position.size_usd = size_usd;
position.collateral_usd = collateral_usd;
position.unrealized_profit_usd = 0;
position.unrealized_loss_usd = 0;
position.cumulative_interest_snapshot = custody.get_cumulative_interest(curtime)?;
position.locked_amount = math::checked_as_u64(math::checked_div(
math::checked_mul(params.size as u128, custody.pricing.max_payoff_mult as u128)?,
Perpetuals::BPS_POWER,
)?)?;
position.collateral_amount = params.collateral;
position.bump = *ctx
.bumps
.get("position")
.ok_or(ProgramError::InvalidSeeds)?;
msg!("Check position risks");
require!(
position.locked_amount > 0,
PerpetualsError::InsufficientAmountReturned
);
require!(
pool.check_leverage(position, &token_ema_price, custody, curtime, true)?,
PerpetualsError::MaxLeverage
);
custody.lock_funds(position.locked_amount)?;
msg!("Transfer tokens");
perpetuals.transfer_tokens_from_user(
ctx.accounts.funding_account.to_account_info(),
ctx.accounts.custody_token_account.to_account_info(),
ctx.accounts.owner.to_account_info(),
ctx.accounts.token_program.to_account_info(),
transfer_amount,
)?;
msg!("Update custody stats");
custody.collected_fees.open_position_usd = custody
.collected_fees
.open_position_usd
.wrapping_add(token_ema_price.get_asset_amount_usd(fee_amount, custody.decimals)?);
custody.volume_stats.open_position_usd = custody
.volume_stats
.open_position_usd
.wrapping_add(size_usd);
custody.assets.collateral = math::checked_add(custody.assets.collateral, params.collateral)?;
let protocol_fee = Pool::get_fee_amount(custody.fees.protocol_share, fee_amount)?;
custody.assets.protocol_fees = math::checked_add(custody.assets.protocol_fees, protocol_fee)?;
if params.side == Side::Long {
custody.trade_stats.oi_long_usd =
math::checked_add(custody.trade_stats.oi_long_usd, size_usd)?;
} else {
custody.trade_stats.oi_short_usd =
math::checked_add(custody.trade_stats.oi_short_usd, size_usd)?;
}
custody.add_position(position, &token_ema_price, curtime)?;
custody.update_borrow_rate(curtime)?;
Ok(())
}