From c1dd383fb5758d520c5b2b7c1d0dca19a56c7df0 Mon Sep 17 00:00:00 2001 From: hjchan Date: Sun, 5 Mar 2023 21:56:45 +0800 Subject: [PATCH 1/7] able to update position --- .../src/instructions/add_collateral.rs | 125 +++++++++++++++--- .../src/instructions/open_position.rs | 2 +- .../src/instructions/remove_collateral.rs | 2 +- programs/perpetuals/tests/anchor/basic.ts | 25 ++++ .../perpetuals/tests/anchor/test_client.ts | 4 + 5 files changed, 141 insertions(+), 17 deletions(-) diff --git a/programs/perpetuals/src/instructions/add_collateral.rs b/programs/perpetuals/src/instructions/add_collateral.rs index d44c733f..e0b53c59 100644 --- a/programs/perpetuals/src/instructions/add_collateral.rs +++ b/programs/perpetuals/src/instructions/add_collateral.rs @@ -5,13 +5,15 @@ use { error::PerpetualsError, math, state::{ - custody::Custody, oracle::OraclePrice, perpetuals::Perpetuals, pool::Pool, - position::Position, + 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)] @@ -89,17 +91,28 @@ pub struct AddCollateral<'info> { #[derive(AnchorSerialize, AnchorDeserialize)] pub struct AddCollateralParams { - collateral: u64, + pub price: u64, + pub collateral: u64, + pub size: u64, } pub fn add_collateral(ctx: Context, params: &AddCollateralParams) -> Result<()> { + // check permissions + msg!("Check permissions"); + let perpetuals = ctx.accounts.perpetuals.as_mut(); + let custody = ctx.accounts.custody.as_mut(); + if params.size > 0 { + require!( + perpetuals.permissions.allow_open_position && custody.permissions.allow_open_position, + PerpetualsError::InstructionNotAllowed + ); + } + // validate inputs msg!("Validate inputs"); - if params.collateral == 0 { + if params.collateral == 0 && (params.price == 0 || params.size == 0) { return Err(ProgramError::InvalidArgument.into()); } - let perpetuals = ctx.accounts.perpetuals.as_mut(); - let custody = ctx.accounts.custody.as_mut(); let position = ctx.accounts.position.as_mut(); let pool = ctx.accounts.pool.as_mut(); let token_id = pool.get_token_id(&custody.key())?; @@ -125,16 +138,49 @@ pub fn add_collateral(ctx: Context, params: &AddCollateralParams) custody.pricing.use_ema, )?; + let position_price = + pool.get_entry_price(&token_price, &token_ema_price, position.side, custody)?; + msg!("Entry price: {}", position_price); + + if params.size > 0 { + if position.side == Side::Long { + require_gte!( + params.price, + position_price, + PerpetualsError::MaxPriceSlippage + ); + } else { + require_gte!( + position_price, + params.price, + PerpetualsError::MaxPriceSlippage + ); + } + } + // compute fee - let fee_amount = - pool.get_add_liquidity_fee(token_id, params.collateral, custody, &token_price)?; + let mut fee_amount = pool.get_entry_fee( + token_id, + params.collateral, + params.size, + custody, + &token_price, + )?; + // collect interest fee and reset cumulative_interest_snapshot + if params.size > 0 { + let interest_usd = custody.get_interest_amount_usd(position, curtime)?; + let interest_amount = token_price.get_token_amount(interest_usd, custody.decimals)?; + + fee_amount = fee_amount + interest_amount; + // remove position here cause borrow fees collected + // will be reopen later + custody.remove_position(position, curtime)?; + } msg!("Collected fee: {}", fee_amount); // compute amount to transfer let transfer_amount = math::checked_add(params.collateral, fee_amount)?; - let collateral_usd = token_price.get_asset_amount_usd(params.collateral, custody.decimals)?; msg!("Amount in: {}", transfer_amount); - msg!("Collateral added in USD: {}", collateral_usd); // check pool constraints msg!("Check pool constraints"); @@ -147,9 +193,37 @@ pub fn add_collateral(ctx: Context, params: &AddCollateralParams) // update existing position msg!("Update existing position"); - position.update_time = perpetuals.get_time()?; - position.collateral_usd = math::checked_add(position.collateral_usd, collateral_usd)?; - position.collateral_amount = math::checked_add(position.collateral_amount, params.collateral)?; + let size_usd = token_price.get_asset_amount_usd(params.size, custody.decimals)?; + let collateral_usd = token_price.get_asset_amount_usd(params.collateral, custody.decimals)?; + msg!("params Collateral added in USD: {}", params.collateral); + msg!("Collateral added in USD: {}", collateral_usd); + let additional_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.update_time = curtime; + + if params.size > 0 { + // (current size * price + new size * new price) / + position.price = math::checked_as_u64(math::checked_div( + math::checked_add( + math::checked_mul(position.size_usd as u128, position.price as u128)?, + math::checked_mul(params.size as u128, position_price as u128)?, + )?, + math::checked_add(position.size_usd as u128, params.size as u128)?, + )?)?; + position.size_usd = math::checked_add(position.size_usd, size_usd)?; + position.locked_amount = + math::checked_add(position.locked_amount, additional_locked_amount)?; + position.cumulative_interest_snapshot = custody.get_cumulative_interest(curtime)?; + } + + if params.collateral > 0 { + position.collateral_usd = math::checked_add(position.collateral_usd, collateral_usd)?; + position.collateral_amount = + math::checked_add(position.collateral_amount, params.collateral)?; + } // check position risk msg!("Check position risks"); @@ -166,6 +240,9 @@ pub fn add_collateral(ctx: Context, params: &AddCollateralParams) PerpetualsError::MaxLeverage ); + // lock funds for potential profit payoff + custody.lock_funds(additional_locked_amount)?; + // transfer tokens msg!("Transfer tokens"); perpetuals.transfer_tokens_from_user( @@ -182,11 +259,29 @@ pub fn add_collateral(ctx: Context, params: &AddCollateralParams) .collected_fees .open_position_usd .wrapping_add(token_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)?; custody.assets.protocol_fees = math::checked_add(custody.assets.protocol_fees, protocol_fee)?; - custody.add_collateral(position.side, collateral_usd)?; + if position.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)?; + } + + if params.size > 0 { + custody.add_position(position, curtime)?; + } else if params.collateral > 0 { + custody.add_collateral(position.side, collateral_usd)?; + } + + custody.update_borrow_rate(curtime)?; Ok(()) } diff --git a/programs/perpetuals/src/instructions/open_position.rs b/programs/perpetuals/src/instructions/open_position.rs index 151eeb0d..38da5544 100644 --- a/programs/perpetuals/src/instructions/open_position.rs +++ b/programs/perpetuals/src/instructions/open_position.rs @@ -190,7 +190,7 @@ pub fn open_position(ctx: Context, params: &OpenPositionParams) -> position.owner = ctx.accounts.owner.key(); position.pool = pool.key(); position.custody = custody.key(); - position.open_time = perpetuals.get_time()?; + position.open_time = curtime; position.update_time = 0; position.side = params.side; position.price = position_price; diff --git a/programs/perpetuals/src/instructions/remove_collateral.rs b/programs/perpetuals/src/instructions/remove_collateral.rs index f4692f50..a2283339 100644 --- a/programs/perpetuals/src/instructions/remove_collateral.rs +++ b/programs/perpetuals/src/instructions/remove_collateral.rs @@ -159,7 +159,7 @@ pub fn remove_collateral( // update existing position msg!("Update existing position"); - position.update_time = perpetuals.get_time()?; + position.update_time = curtime; position.collateral_usd = math::checked_sub(position.collateral_usd, params.collateral_usd)?; position.collateral_amount = math::checked_sub(position.collateral_amount, collateral)?; diff --git a/programs/perpetuals/tests/anchor/basic.ts b/programs/perpetuals/tests/anchor/basic.ts index 408e30c0..a97febc6 100644 --- a/programs/perpetuals/tests/anchor/basic.ts +++ b/programs/perpetuals/tests/anchor/basic.ts @@ -456,12 +456,37 @@ describe("perpetuals", () => { it("addCollateral", async () => { await tc.addCollateral( + 125, tc.toTokenAmount(1, tc.custodies[0].decimals), + tc.toTokenAmount(2, tc.custodies[0].decimals), tc.users[0], tc.users[0].tokenAccounts[0], tc.users[0].positionAccountsLong[0], tc.custodies[0] ); + + let position = await tc.program.account.position.fetch( + tc.users[0].positionAccountsLong[0] + ); + positionExpected = { + owner: tc.users[0].wallet.publicKey.toBase58(), + pool: tc.pool.publicKey.toBase58(), + custody: tc.custodies[0].custody.toBase58(), + openTime: "111", + updateTime: "111", + side: { long: {} }, + price: "124230000", + sizeUsd: "1107000000", + collateralUsd: "246000000", + unrealizedProfitUsd: "0", + unrealizedLossUsd: "0", + cumulativeInterestSnapshot: "0", + lockedAmount: "9000000000", + collateralAmount: "2000000000", + bump: position.bump, + }; + console.log({ position }); + expect(JSON.stringify(position)).to.equal(JSON.stringify(positionExpected)); }); it("removeCollateral", async () => { diff --git a/programs/perpetuals/tests/anchor/test_client.ts b/programs/perpetuals/tests/anchor/test_client.ts index 9edb2de3..18895357 100644 --- a/programs/perpetuals/tests/anchor/test_client.ts +++ b/programs/perpetuals/tests/anchor/test_client.ts @@ -856,7 +856,9 @@ export class TestClient { }; addCollateral = async ( + price: number, collateral: typeof BN, + size: typeof BN, user, fundingAccount: PublicKey, positionAccount: PublicKey, @@ -865,7 +867,9 @@ export class TestClient { try { await this.program.methods .addCollateral({ + price: new BN(price * 1000000), collateral, + size, }) .accounts({ owner: user.wallet.publicKey, From 1015382ebf31e1be77e898fb8165154609850420 Mon Sep 17 00:00:00 2001 From: hjchan Date: Mon, 6 Mar 2023 01:42:54 +0800 Subject: [PATCH 2/7] remove console log --- programs/perpetuals/tests/anchor/basic.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/programs/perpetuals/tests/anchor/basic.ts b/programs/perpetuals/tests/anchor/basic.ts index a97febc6..f952a1bf 100644 --- a/programs/perpetuals/tests/anchor/basic.ts +++ b/programs/perpetuals/tests/anchor/basic.ts @@ -485,7 +485,6 @@ describe("perpetuals", () => { collateralAmount: "2000000000", bump: position.bump, }; - console.log({ position }); expect(JSON.stringify(position)).to.equal(JSON.stringify(positionExpected)); }); From ff960dbb5d463db3ebd53a91b53680196b853ca8 Mon Sep 17 00:00:00 2001 From: hjchan Date: Wed, 8 Mar 2023 11:39:49 +0800 Subject: [PATCH 3/7] add interest to unrealized loss usd --- .../src/instructions/add_collateral.rs | 29 ++++++++----------- .../src/instructions/open_position.rs | 1 + 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/programs/perpetuals/src/instructions/add_collateral.rs b/programs/perpetuals/src/instructions/add_collateral.rs index e0b53c59..78c2a9b3 100644 --- a/programs/perpetuals/src/instructions/add_collateral.rs +++ b/programs/perpetuals/src/instructions/add_collateral.rs @@ -159,23 +159,13 @@ pub fn add_collateral(ctx: Context, params: &AddCollateralParams) } // compute fee - let mut fee_amount = pool.get_entry_fee( + let fee_amount = pool.get_entry_fee( token_id, params.collateral, params.size, custody, &token_price, )?; - // collect interest fee and reset cumulative_interest_snapshot - if params.size > 0 { - let interest_usd = custody.get_interest_amount_usd(position, curtime)?; - let interest_amount = token_price.get_token_amount(interest_usd, custody.decimals)?; - - fee_amount = fee_amount + interest_amount; - // remove position here cause borrow fees collected - // will be reopen later - custody.remove_position(position, curtime)?; - } msg!("Collected fee: {}", fee_amount); // compute amount to transfer @@ -205,6 +195,15 @@ pub fn add_collateral(ctx: Context, params: &AddCollateralParams) position.update_time = curtime; if params.size > 0 { + // calculate interest fee and reset cumulative_interest_snapshot + let interest_usd = custody.get_interest_amount_usd(position, curtime)?; + position.unrealized_loss_usd = + math::checked_add(position.unrealized_loss_usd, interest_usd)?; + position.cumulative_interest_snapshot = custody.get_cumulative_interest(curtime)?; + // remove position here cause borrow fees collected + // will be reopen later + custody.remove_position(position, curtime)?; + // (current size * price + new size * new price) / position.price = math::checked_as_u64(math::checked_div( math::checked_add( @@ -216,14 +215,10 @@ pub fn add_collateral(ctx: Context, params: &AddCollateralParams) position.size_usd = math::checked_add(position.size_usd, size_usd)?; position.locked_amount = math::checked_add(position.locked_amount, additional_locked_amount)?; - position.cumulative_interest_snapshot = custody.get_cumulative_interest(curtime)?; } - if params.collateral > 0 { - position.collateral_usd = math::checked_add(position.collateral_usd, collateral_usd)?; - position.collateral_amount = - math::checked_add(position.collateral_amount, params.collateral)?; - } + position.collateral_usd = math::checked_add(position.collateral_usd, collateral_usd)?; + position.collateral_amount = math::checked_add(position.collateral_amount, params.collateral)?; // check position risk msg!("Check position risks"); diff --git a/programs/perpetuals/src/instructions/open_position.rs b/programs/perpetuals/src/instructions/open_position.rs index 38da5544..0560e6e4 100644 --- a/programs/perpetuals/src/instructions/open_position.rs +++ b/programs/perpetuals/src/instructions/open_position.rs @@ -186,6 +186,7 @@ pub fn open_position(ctx: Context, params: &OpenPositionParams) -> msg!("Initialize new position"); let size_usd = token_price.get_asset_amount_usd(params.size, custody.decimals)?; let collateral_usd = token_price.get_asset_amount_usd(params.collateral, custody.decimals)?; + msg!("Collateral added in USD: {}", collateral_usd); position.owner = ctx.accounts.owner.key(); position.pool = pool.key(); From bda70508628290087c142bc47269cebb6aac9195 Mon Sep 17 00:00:00 2001 From: hjchan Date: Wed, 15 Mar 2023 14:57:52 +0700 Subject: [PATCH 4/7] update add collateral to increase position --- programs/perpetuals/src/instructions.rs | 4 ++-- .../{add_collateral.rs => increase_position.rs} | 15 +++++++++------ programs/perpetuals/src/lib.rs | 7 +++++-- programs/perpetuals/tests/anchor/basic.ts | 4 ++-- programs/perpetuals/tests/anchor/test_client.ts | 4 ++-- 5 files changed, 20 insertions(+), 14 deletions(-) rename programs/perpetuals/src/instructions/{add_collateral.rs => increase_position.rs} (96%) diff --git a/programs/perpetuals/src/instructions.rs b/programs/perpetuals/src/instructions.rs index f6ed43ff..52de73ce 100644 --- a/programs/perpetuals/src/instructions.rs +++ b/programs/perpetuals/src/instructions.rs @@ -17,7 +17,6 @@ pub mod set_test_time; pub mod test_init; // public instructions -pub mod add_collateral; pub mod add_liquidity; pub mod close_position; pub mod get_assets_under_management; @@ -28,6 +27,7 @@ pub mod get_liquidation_state; pub mod get_oracle_price; pub mod get_pnl; pub mod get_swap_amount_and_fees; +pub mod increase_position; pub mod liquidate; pub mod open_position; pub mod remove_collateral; @@ -51,7 +51,6 @@ pub use set_test_oracle_price::*; pub use set_test_time::*; pub use test_init::*; -pub use add_collateral::*; pub use add_liquidity::*; pub use close_position::*; pub use get_assets_under_management::*; @@ -62,6 +61,7 @@ pub use get_liquidation_state::*; pub use get_oracle_price::*; pub use get_pnl::*; pub use get_swap_amount_and_fees::*; +pub use increase_position::*; pub use liquidate::*; pub use open_position::*; pub use remove_collateral::*; diff --git a/programs/perpetuals/src/instructions/add_collateral.rs b/programs/perpetuals/src/instructions/increase_position.rs similarity index 96% rename from programs/perpetuals/src/instructions/add_collateral.rs rename to programs/perpetuals/src/instructions/increase_position.rs index f9ff0d84..d56d4f5f 100644 --- a/programs/perpetuals/src/instructions/add_collateral.rs +++ b/programs/perpetuals/src/instructions/increase_position.rs @@ -1,4 +1,4 @@ -//! AddCollateral instruction handler +//! IncreasePosition instruction handler use { crate::{ @@ -17,8 +17,8 @@ use { }; #[derive(Accounts)] -#[instruction(params: AddCollateralParams)] -pub struct AddCollateral<'info> { +#[instruction(params: IncreasePositionParams)] +pub struct IncreasePosition<'info> { #[account(mut)] pub owner: Signer<'info>, @@ -90,13 +90,16 @@ pub struct AddCollateral<'info> { } #[derive(AnchorSerialize, AnchorDeserialize)] -pub struct AddCollateralParams { +pub struct IncreasePositionParams { pub price: u64, pub collateral: u64, pub size: u64, } -pub fn add_collateral(ctx: Context, params: &AddCollateralParams) -> Result<()> { +pub fn increase_position( + ctx: Context, + params: &IncreasePositionParams, +) -> Result<()> { // check permissions msg!("Check permissions"); let perpetuals = ctx.accounts.perpetuals.as_mut(); @@ -270,7 +273,7 @@ pub fn add_collateral(ctx: Context, params: &AddCollateralParams) } if params.size > 0 { - custody.add_position(position, curtime)?; + custody.add_position(position, &token_price, curtime)?; } else if params.collateral > 0 { custody.add_collateral(position.side, collateral_usd)?; } diff --git a/programs/perpetuals/src/lib.rs b/programs/perpetuals/src/lib.rs index 6d7acb48..bf86de45 100644 --- a/programs/perpetuals/src/lib.rs +++ b/programs/perpetuals/src/lib.rs @@ -144,8 +144,11 @@ pub mod perpetuals { instructions::open_position(ctx, ¶ms) } - pub fn add_collateral(ctx: Context, params: AddCollateralParams) -> Result<()> { - instructions::add_collateral(ctx, ¶ms) + pub fn increase_position( + ctx: Context, + params: IncreasePositionParams, + ) -> Result<()> { + instructions::increase_position(ctx, ¶ms) } pub fn remove_collateral( diff --git a/programs/perpetuals/tests/anchor/basic.ts b/programs/perpetuals/tests/anchor/basic.ts index 53bb070f..69f15e99 100644 --- a/programs/perpetuals/tests/anchor/basic.ts +++ b/programs/perpetuals/tests/anchor/basic.ts @@ -462,8 +462,8 @@ describe("perpetuals", () => { expect(JSON.stringify(position)).to.equal(JSON.stringify(positionExpected)); }); - it("addCollateral", async () => { - await tc.addCollateral( + it("increasePosition", async () => { + await tc.increasePosition( 125, tc.toTokenAmount(1, tc.custodies[0].decimals), tc.toTokenAmount(2, tc.custodies[0].decimals), diff --git a/programs/perpetuals/tests/anchor/test_client.ts b/programs/perpetuals/tests/anchor/test_client.ts index 3403d095..4b2189da 100644 --- a/programs/perpetuals/tests/anchor/test_client.ts +++ b/programs/perpetuals/tests/anchor/test_client.ts @@ -875,7 +875,7 @@ export class TestClient { } }; - addCollateral = async ( + increasePosition = async ( price: number, collateral: typeof BN, size: typeof BN, @@ -886,7 +886,7 @@ export class TestClient { ) => { try { await this.program.methods - .addCollateral({ + .increasePosition({ price: new BN(price * 1000000), collateral, size, From bd53d1e05904de00fa4c43966459730e6aaa912d Mon Sep 17 00:00:00 2001 From: hjchan Date: Wed, 15 Mar 2023 17:55:41 +0700 Subject: [PATCH 5/7] fix wrong price calculation --- programs/perpetuals/src/instructions/increase_position.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/programs/perpetuals/src/instructions/increase_position.rs b/programs/perpetuals/src/instructions/increase_position.rs index d56d4f5f..8f8cfc19 100644 --- a/programs/perpetuals/src/instructions/increase_position.rs +++ b/programs/perpetuals/src/instructions/increase_position.rs @@ -188,7 +188,7 @@ pub fn increase_position( msg!("Update existing position"); let size_usd = token_price.get_asset_amount_usd(params.size, custody.decimals)?; let collateral_usd = token_price.get_asset_amount_usd(params.collateral, custody.decimals)?; - msg!("params Collateral added in USD: {}", params.collateral); + msg!("Params Collateral added: {}", params.collateral); msg!("Collateral added in USD: {}", collateral_usd); let additional_locked_amount = math::checked_as_u64(math::checked_div( math::checked_mul(params.size as u128, custody.pricing.max_payoff_mult as u128)?, @@ -207,11 +207,11 @@ pub fn increase_position( // will be reopen later custody.remove_position(position, curtime)?; - // (current size * price + new size * new price) / + // (current size * price + new size * new price) / (current size + new size) position.price = math::checked_as_u64(math::checked_div( math::checked_add( math::checked_mul(position.size_usd as u128, position.price as u128)?, - math::checked_mul(params.size as u128, position_price as u128)?, + math::checked_mul(size_usd as u128, position_price as u128)?, )?, math::checked_add(position.size_usd as u128, params.size as u128)?, )?)?; From 1d439eaad6ae8dd3bd922b31246f118132851772 Mon Sep 17 00:00:00 2001 From: hjchan Date: Thu, 16 Mar 2023 13:41:32 +0700 Subject: [PATCH 6/7] get exit fee calculate close position --- .../instructions/get_exit_price_and_fee.rs | 1 + .../src/instructions/remove_collateral.rs | 112 ++++++++++++++++-- programs/perpetuals/src/state/pool.rs | 23 +++- 3 files changed, 120 insertions(+), 16 deletions(-) diff --git a/programs/perpetuals/src/instructions/get_exit_price_and_fee.rs b/programs/perpetuals/src/instructions/get_exit_price_and_fee.rs index e067a3ad..32cff103 100644 --- a/programs/perpetuals/src/instructions/get_exit_price_and_fee.rs +++ b/programs/perpetuals/src/instructions/get_exit_price_and_fee.rs @@ -89,6 +89,7 @@ pub fn get_exit_price_and_fee( let fee = pool.get_exit_fee( pool.get_token_id(&custody.key())?, collateral, + position.size_usd, custody, &token_price, )?; diff --git a/programs/perpetuals/src/instructions/remove_collateral.rs b/programs/perpetuals/src/instructions/remove_collateral.rs index f95800d4..1f3b1ae0 100644 --- a/programs/perpetuals/src/instructions/remove_collateral.rs +++ b/programs/perpetuals/src/instructions/remove_collateral.rs @@ -5,8 +5,11 @@ use { error::PerpetualsError, math, state::{ - custody::Custody, oracle::OraclePrice, perpetuals::Perpetuals, pool::Pool, - position::Position, + custody::Custody, + oracle::OraclePrice, + perpetuals::Perpetuals, + pool::Pool, + position::{Position, Side}, }, }, anchor_lang::prelude::*, @@ -89,7 +92,9 @@ pub struct RemoveCollateral<'info> { #[derive(AnchorSerialize, AnchorDeserialize)] pub struct RemoveCollateralParams { - collateral_usd: u64, + pub price: u64, + pub collateral_usd: u64, + pub size_usd: u64, } pub fn remove_collateral( @@ -100,16 +105,32 @@ pub fn remove_collateral( msg!("Check permissions"); let perpetuals = ctx.accounts.perpetuals.as_mut(); let custody = ctx.accounts.custody.as_mut(); - require!( - perpetuals.permissions.allow_collateral_withdrawal - && custody.permissions.allow_collateral_withdrawal, - PerpetualsError::InstructionNotAllowed - ); + if params.collateral_usd > 0 { + require!( + perpetuals.permissions.allow_collateral_withdrawal + && custody.permissions.allow_collateral_withdrawal, + PerpetualsError::InstructionNotAllowed + ); + } + if params.size_usd > 0 { + require!( + perpetuals.permissions.allow_close_position && custody.permissions.allow_close_position, + PerpetualsError::InstructionNotAllowed + ); + } // validate inputs msg!("Validate inputs"); let position = ctx.accounts.position.as_mut(); - if params.collateral_usd == 0 || params.collateral_usd >= position.collateral_usd { + if params.collateral_usd == 0 && (params.price == 0 || params.size_usd == 0) { + return Err(ProgramError::InvalidArgument.into()); + } + if params.collateral_usd > 0 + && params.collateral_usd >= (position.size_usd - position.collateral_usd) + { + return Err(ProgramError::InvalidArgument.into()); + } + if params.size_usd >= position.size_usd { return Err(ProgramError::InvalidArgument.into()); } let pool = ctx.accounts.pool.as_mut(); @@ -136,9 +157,24 @@ pub fn remove_collateral( custody.pricing.use_ema, )?; + let exit_price = pool.get_exit_price(&token_price, &token_ema_price, position.side, custody)?; + msg!("Exit price: {}", exit_price); + + if position.side == Side::Long { + require_gte!(exit_price, params.price, PerpetualsError::MaxPriceSlippage); + } else { + require_gte!(params.price, exit_price, PerpetualsError::MaxPriceSlippage); + } + // compute fee let collateral = token_price.get_token_amount(params.collateral_usd, custody.decimals)?; - let fee_amount = pool.get_remove_liquidity_fee(token_id, collateral, custody, &token_price)?; + let fee_amount = pool.get_exit_fee( + token_id, + collateral, + position.size_usd, + custody, + &token_price, + )?; msg!("Collected fee: {}", fee_amount); // compute amount to transfer @@ -159,7 +195,36 @@ pub fn remove_collateral( // update existing position msg!("Update existing position"); + let size = token_price.get_token_amount(params.size_usd, custody.decimals)?; + let release_locked_amount = math::checked_as_u64(math::checked_div( + math::checked_mul(size as u128, custody.pricing.max_payoff_mult as u128)?, + Perpetuals::BPS_POWER, + )?)?; + position.update_time = curtime; + + if params.size_usd > 0 { + // calculate interest fee and reset cumulative_interest_snapshot + let interest_usd = custody.get_interest_amount_usd(position, curtime)?; + position.unrealized_loss_usd = + math::checked_add(position.unrealized_loss_usd, interest_usd)?; + position.cumulative_interest_snapshot = custody.get_cumulative_interest(curtime)?; + // remove position here cause borrow fees collected + // will be reopen later + custody.remove_position(position, curtime)?; + + // (current size * price - removed size * new price) / (current size - removed size) + position.price = math::checked_as_u64(math::checked_div( + math::checked_sub( + math::checked_mul(position.size_usd as u128, position.price as u128)?, + math::checked_mul(params.size_usd as u128, exit_price as u128)?, + )?, + math::checked_sub(position.size_usd as u128, params.size_usd as u128)?, + )?)?; + position.size_usd = math::checked_sub(position.size_usd, params.size_usd)?; + position.locked_amount = math::checked_sub(position.locked_amount, release_locked_amount)?; + } + position.collateral_usd = math::checked_sub(position.collateral_usd, params.collateral_usd)?; position.collateral_amount = math::checked_sub(position.collateral_amount, collateral)?; @@ -177,6 +242,9 @@ pub fn remove_collateral( PerpetualsError::MaxLeverage ); + // unlock funds + custody.unlock_funds(release_locked_amount)?; + // transfer tokens msg!("Transfer tokens"); perpetuals.transfer_tokens( @@ -189,15 +257,33 @@ pub fn remove_collateral( // update custody stats msg!("Update custody stats"); - custody.collected_fees.open_position_usd = custody + custody.collected_fees.close_position_usd = custody .collected_fees - .open_position_usd + .close_position_usd .wrapping_add(token_price.get_asset_amount_usd(fee_amount, custody.decimals)?); + custody.volume_stats.open_position_usd = custody + .volume_stats + .open_position_usd + .wrapping_sub(params.size_usd); custody.assets.collateral = math::checked_sub(custody.assets.collateral, collateral)?; custody.assets.protocol_fees = math::checked_add(custody.assets.protocol_fees, protocol_fee)?; - custody.remove_collateral(position.side, params.collateral_usd)?; + if position.side == Side::Long { + custody.trade_stats.oi_long_usd = + math::checked_sub(custody.trade_stats.oi_long_usd, params.size_usd)?; + } else { + custody.trade_stats.oi_short_usd = + math::checked_sub(custody.trade_stats.oi_short_usd, params.size_usd)?; + } + + if params.size_usd > 0 { + custody.add_position(position, &token_price, curtime)?; + } else if params.collateral_usd > 0 { + custody.remove_collateral(position.side, params.collateral_usd)?; + } + + custody.update_borrow_rate(curtime)?; Ok(()) } diff --git a/programs/perpetuals/src/state/pool.rs b/programs/perpetuals/src/state/pool.rs index dbd564b3..361fbaba 100644 --- a/programs/perpetuals/src/state/pool.rs +++ b/programs/perpetuals/src/state/pool.rs @@ -118,10 +118,15 @@ impl Pool { &self, token_id: usize, collateral: u64, + size: u64, custody: &Custody, token_price: &OraclePrice, ) -> Result { - self.get_remove_liquidity_fee(token_id, collateral, custody, token_price) + let collateral_fee = + self.get_remove_liquidity_fee(token_id, collateral, custody, token_price)?; + let size_fee = Self::get_fee_amount(custody.fees.close_position, size)?; + + math::checked_add(collateral_fee, size_fee) } #[allow(clippy::too_many_arguments)] @@ -393,7 +398,13 @@ impl Pool { // liq_price_short = pos_price + (collateral + unreal_profit - (exit_fee + unreal_loss + size / max_leverage)) / init_leverage - spread let collateral = token_price.get_token_amount(position.collateral_usd, custody.decimals)?; - let exit_fee_tokens = self.get_exit_fee(token_id, collateral, custody, token_price)?; + let exit_fee_tokens = self.get_exit_fee( + token_id, + collateral, + position.size_usd, + custody, + token_price, + )?; let exit_fee_usd = token_price.get_asset_amount_usd(exit_fee_tokens, custody.decimals)?; @@ -489,7 +500,13 @@ impl Pool { let exit_fee = if liquidation { self.get_liquidation_fee(token_id, collateral, custody, token_price)? } else { - self.get_exit_fee(token_id, collateral, custody, token_price)? + self.get_exit_fee( + token_id, + collateral, + position.size_usd, + custody, + token_price, + )? }; let exit_fee_usd = token_price.get_asset_amount_usd(exit_fee, custody.decimals)?; From f322ccba508b58719237e7f23c8a80c5bcb64462 Mon Sep 17 00:00:00 2001 From: hjchan Date: Wed, 22 Mar 2023 14:39:28 +0800 Subject: [PATCH 7/7] decrease position --- programs/perpetuals/src/instructions.rs | 4 +- .../src/instructions/close_position.rs | 3 +- ...ove_collateral.rs => decrease_position.rs} | 75 ++++++++++++------- .../instructions/get_entry_price_and_fee.rs | 3 +- .../instructions/get_exit_price_and_fee.rs | 2 +- .../src/instructions/increase_position.rs | 2 +- .../src/instructions/open_position.rs | 2 +- programs/perpetuals/src/lib.rs | 8 +- programs/perpetuals/src/state/pool.rs | 55 +++++++++++++- 9 files changed, 112 insertions(+), 42 deletions(-) rename programs/perpetuals/src/instructions/{remove_collateral.rs => decrease_position.rs} (80%) diff --git a/programs/perpetuals/src/instructions.rs b/programs/perpetuals/src/instructions.rs index 3e86247e..5eb8ab3f 100644 --- a/programs/perpetuals/src/instructions.rs +++ b/programs/perpetuals/src/instructions.rs @@ -19,6 +19,7 @@ pub mod test_init; // public instructions pub mod add_liquidity; pub mod close_position; +pub mod decrease_position; pub mod get_add_liquidity_amount_and_fee; pub mod get_assets_under_management; pub mod get_entry_price_and_fee; @@ -32,7 +33,6 @@ pub mod get_swap_amount_and_fees; pub mod increase_position; pub mod liquidate; pub mod open_position; -pub mod remove_collateral; pub mod remove_liquidity; pub mod swap; @@ -55,6 +55,7 @@ pub use test_init::*; pub use add_liquidity::*; pub use close_position::*; +pub use decrease_position::*; pub use get_add_liquidity_amount_and_fee::*; pub use get_assets_under_management::*; pub use get_entry_price_and_fee::*; @@ -68,6 +69,5 @@ pub use get_swap_amount_and_fees::*; pub use increase_position::*; pub use liquidate::*; pub use open_position::*; -pub use remove_collateral::*; pub use remove_liquidity::*; pub use swap::*; diff --git a/programs/perpetuals/src/instructions/close_position.rs b/programs/perpetuals/src/instructions/close_position.rs index b99e76f0..758891b8 100644 --- a/programs/perpetuals/src/instructions/close_position.rs +++ b/programs/perpetuals/src/instructions/close_position.rs @@ -134,7 +134,8 @@ pub fn close_position(ctx: Context, params: &ClosePositionParams) custody.pricing.use_ema, )?; - let exit_price = pool.get_exit_price(&token_price, &token_ema_price, position.side, custody)?; + let (_, exit_price) = + pool.get_exit_price(&token_price, &token_ema_price, position.side, custody)?; msg!("Exit price: {}", exit_price); if position.side == Side::Long { diff --git a/programs/perpetuals/src/instructions/remove_collateral.rs b/programs/perpetuals/src/instructions/decrease_position.rs similarity index 80% rename from programs/perpetuals/src/instructions/remove_collateral.rs rename to programs/perpetuals/src/instructions/decrease_position.rs index 05c0d1cb..cc56b665 100644 --- a/programs/perpetuals/src/instructions/remove_collateral.rs +++ b/programs/perpetuals/src/instructions/decrease_position.rs @@ -1,4 +1,4 @@ -//! RemoveCollateral instruction handler +//! DecreasePosition instruction handler use { crate::{ @@ -18,8 +18,8 @@ use { }; #[derive(Accounts)] -#[instruction(params: RemoveCollateralParams)] -pub struct RemoveCollateral<'info> { +#[instruction(params: DecreasePositionParams)] +pub struct DecreasePosition<'info> { #[account(mut)] pub owner: Signer<'info>, @@ -91,15 +91,15 @@ pub struct RemoveCollateral<'info> { } #[derive(AnchorSerialize, AnchorDeserialize)] -pub struct RemoveCollateralParams { +pub struct DecreasePositionParams { pub price: u64, pub collateral_usd: u64, pub size_usd: u64, } -pub fn remove_collateral( - ctx: Context, - params: &RemoveCollateralParams, +pub fn decrease_position( + ctx: Context, + params: &DecreasePositionParams, ) -> Result<()> { // check permissions msg!("Check permissions"); @@ -166,23 +166,43 @@ pub fn remove_collateral( } else { require_gte!(params.price, exit_price, PerpetualsError::MaxPriceSlippage); } + custody.remove_position(position, curtime)?; // compute fee - let collateral = exit_oracle.get_token_amount(params.collateral_usd, custody.decimals)?; + let (profit_usd, loss_usd) = pool.get_pnl_usd_for_size( + position, + &token_price, + &token_ema_price, + custody, + params.size_usd, + )?; + let mut transfer_amount = params.collateral_usd; + if profit_usd > 0 { + transfer_amount = math::checked_add(transfer_amount, profit_usd)?; + } else if loss_usd > 0 { + let loss_token = exit_oracle.get_token_amount(loss_usd, custody.decimals)?; + + position.collateral_usd = math::checked_sub(position.collateral_usd, loss_usd)?; + position.collateral_amount = math::checked_sub(position.collateral_amount, loss_token)?; + } + + let transfer_token_amount = exit_oracle.get_token_amount(transfer_amount, custody.decimals)?; let fee_amount = pool.get_exit_fee( token_id, - collateral, + transfer_token_amount, position.size_usd, custody, &exit_oracle, )?; + let protocol_fee = Pool::get_fee_amount(custody.fees.protocol_share, fee_amount)?; + msg!("Collected fee: {}", fee_amount); // compute amount to transfer - if collateral > position.collateral_amount { + let transfer_amount = math::checked_sub(transfer_token_amount, fee_amount)?; + if transfer_amount > position.collateral_amount { return Err(ProgramError::InsufficientFunds.into()); } - let transfer_amount = math::checked_sub(collateral, fee_amount)?; msg!("Amount out: {}", transfer_amount); // update existing position @@ -201,9 +221,6 @@ pub fn remove_collateral( position.unrealized_loss_usd = math::checked_add(position.unrealized_loss_usd, interest_usd)?; position.cumulative_interest_snapshot = custody.get_cumulative_interest(curtime)?; - // remove position here cause borrow fees collected - // will be reopen later - custody.remove_position(position, curtime)?; // (current size * price - removed size * new price) / (current size - removed size) position.price = math::checked_as_u64(math::checked_div( @@ -218,6 +235,8 @@ pub fn remove_collateral( } position.collateral_usd = math::checked_sub(position.collateral_usd, params.collateral_usd)?; + + let collateral = exit_oracle.get_token_amount(params.collateral_usd, custody.decimals)?; position.collateral_amount = math::checked_sub(position.collateral_amount, collateral)?; // check position risk @@ -246,30 +265,32 @@ pub fn remove_collateral( .collected_fees .close_position_usd .wrapping_add(token_ema_price.get_asset_amount_usd(fee_amount, custody.decimals)?); - custody.volume_stats.open_position_usd = custody + custody.volume_stats.close_position_usd = custody .volume_stats - .open_position_usd + .close_position_usd .wrapping_sub(params.size_usd); + let amount_lost = transfer_amount.saturating_sub(collateral); + custody.assets.owned = math::checked_sub(custody.assets.owned, amount_lost)?; custody.assets.collateral = math::checked_sub(custody.assets.collateral, 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 position.side == Side::Long { - custody.trade_stats.oi_long_usd = - math::checked_sub(custody.trade_stats.oi_long_usd, params.size_usd)?; + custody.trade_stats.oi_long_usd = custody + .trade_stats + .oi_long_usd + .saturating_sub(params.size_usd); } else { - custody.trade_stats.oi_short_usd = - math::checked_sub(custody.trade_stats.oi_short_usd, params.size_usd)?; + custody.trade_stats.oi_short_usd = custody + .trade_stats + .oi_short_usd + .saturating_sub(params.size_usd); } - if params.size_usd > 0 { - custody.add_position(position, &token_price, curtime)?; - } else if params.collateral_usd > 0 { - custody.remove_collateral(position.side, params.collateral_usd)?; - } + custody.trade_stats.profit_usd = custody.trade_stats.profit_usd.wrapping_add(profit_usd); + custody.trade_stats.loss_usd = custody.trade_stats.loss_usd.wrapping_add(loss_usd); + custody.add_position(position, &token_price, curtime)?; custody.update_borrow_rate(curtime)?; Ok(()) diff --git a/programs/perpetuals/src/instructions/get_entry_price_and_fee.rs b/programs/perpetuals/src/instructions/get_entry_price_and_fee.rs index c9af0e73..806526a2 100644 --- a/programs/perpetuals/src/instructions/get_entry_price_and_fee.rs +++ b/programs/perpetuals/src/instructions/get_entry_price_and_fee.rs @@ -82,7 +82,8 @@ pub fn get_entry_price_and_fee( custody.pricing.use_ema, )?; - let entry_price = pool.get_entry_price(&token_price, &token_ema_price, params.side, custody)?; + let (_, entry_price) = + pool.get_entry_price(&token_price, &token_ema_price, params.side, custody)?; let size_usd = token_price.get_asset_amount_usd(params.size, custody.decimals)?; let collateral_usd = token_price.get_asset_amount_usd(params.collateral, custody.decimals)?; diff --git a/programs/perpetuals/src/instructions/get_exit_price_and_fee.rs b/programs/perpetuals/src/instructions/get_exit_price_and_fee.rs index 32cff103..e35236ab 100644 --- a/programs/perpetuals/src/instructions/get_exit_price_and_fee.rs +++ b/programs/perpetuals/src/instructions/get_exit_price_and_fee.rs @@ -84,7 +84,7 @@ pub fn get_exit_price_and_fee( let collateral = token_price.get_token_amount(position.collateral_usd, custody.decimals)?; - let price = pool.get_exit_price(&token_price, &token_ema_price, position.side, custody)?; + let (_, price) = pool.get_exit_price(&token_price, &token_ema_price, position.side, custody)?; let fee = pool.get_exit_fee( pool.get_token_id(&custody.key())?, diff --git a/programs/perpetuals/src/instructions/increase_position.rs b/programs/perpetuals/src/instructions/increase_position.rs index 66fc737e..9a834019 100644 --- a/programs/perpetuals/src/instructions/increase_position.rs +++ b/programs/perpetuals/src/instructions/increase_position.rs @@ -141,7 +141,7 @@ pub fn increase_position( custody.pricing.use_ema, )?; - let position_price = + let (_, position_price) = pool.get_entry_price(&token_price, &token_ema_price, position.side, custody)?; msg!("Entry price: {}", position_price); diff --git a/programs/perpetuals/src/instructions/open_position.rs b/programs/perpetuals/src/instructions/open_position.rs index 69b78140..48f54b2b 100644 --- a/programs/perpetuals/src/instructions/open_position.rs +++ b/programs/perpetuals/src/instructions/open_position.rs @@ -143,7 +143,7 @@ pub fn open_position(ctx: Context, params: &OpenPositionParams) -> custody.pricing.use_ema, )?; - let position_price = + let (_, position_price) = pool.get_entry_price(&token_price, &token_ema_price, params.side, custody)?; msg!("Entry price: {}", position_price); diff --git a/programs/perpetuals/src/lib.rs b/programs/perpetuals/src/lib.rs index 20d3dfe9..75247a73 100644 --- a/programs/perpetuals/src/lib.rs +++ b/programs/perpetuals/src/lib.rs @@ -153,11 +153,11 @@ pub mod perpetuals { instructions::increase_position(ctx, ¶ms) } - pub fn remove_collateral( - ctx: Context, - params: RemoveCollateralParams, + pub fn decrease_position( + ctx: Context, + params: DecreasePositionParams, ) -> Result<()> { - instructions::remove_collateral(ctx, ¶ms) + instructions::decrease_position(ctx, ¶ms) } pub fn close_position(ctx: Context, params: ClosePositionParams) -> Result<()> { diff --git a/programs/perpetuals/src/state/pool.rs b/programs/perpetuals/src/state/pool.rs index 4ef2ed48..ec05ee89 100644 --- a/programs/perpetuals/src/state/pool.rs +++ b/programs/perpetuals/src/state/pool.rs @@ -100,7 +100,7 @@ impl Pool { token_ema_price: &OraclePrice, side: Side, custody: &Custody, - ) -> Result { + ) -> Result<(OraclePrice, u64)> { let price = self.get_price( token_price, token_ema_price, @@ -112,9 +112,12 @@ impl Pool { }, )?; - Ok(price - .scale_to_exponent(-(Perpetuals::PRICE_DECIMALS as i32))? - .price) + Ok(( + price, + price + .scale_to_exponent(-(Perpetuals::PRICE_DECIMALS as i32))? + .price, + )) } pub fn get_entry_fee( @@ -652,6 +655,50 @@ impl Pool { } } + pub fn get_pnl_usd_for_size( + &self, + position: &Position, + token_price: &OraclePrice, + token_ema_price: &OraclePrice, + custody: &Custody, + size_usd: u64, + ) -> Result<(u64, u64)> { + if size_usd == 0 { + return Ok((0, 0)); + } + + let (_, exit_price) = + self.get_exit_price(token_price, token_ema_price, position.side, custody)?; + + let (price_diff_profit, price_diff_loss) = if position.side == Side::Long { + if exit_price > position.price { + (math::checked_sub(exit_price, position.price)?, 0u64) + } else { + (0u64, math::checked_sub(position.price, exit_price)?) + } + } else if exit_price < position.price { + (math::checked_sub(position.price, exit_price)?, 0u64) + } else { + (0u64, math::checked_sub(exit_price, position.price)?) + }; + + if price_diff_profit > 0 { + let profit_usd = math::checked_as_u64(math::checked_div( + math::checked_mul(size_usd as u128, price_diff_profit as u128)?, + position.price as u128, + )?)?; + + Ok((profit_usd, 0u64)) + } else { + let loss_usd = math::checked_as_u64(math::checked_div( + math::checked_mul(size_usd as u128, price_diff_loss as u128)?, + position.price as u128, + )?)?; + + Ok((0u64, loss_usd)) + } + } + pub fn get_assets_under_management_usd( &self, aum_calc_mode: AumCalcMode,