Added perp_settle_fees instruction

This commit is contained in:
Conj0iner 2022-09-05 22:18:40 +08:00
parent 3fbc3ca32d
commit c34ee54233
10 changed files with 828 additions and 29 deletions

View File

@ -20,6 +20,7 @@ pub use perp_consume_events::*;
pub use perp_create_market::*;
pub use perp_edit_market::*;
pub use perp_place_order::*;
pub use perp_settle_fees::*;
pub use perp_settle_pnl::*;
pub use perp_update_funding::*;
pub use serum3_cancel_all_orders::*;
@ -65,6 +66,7 @@ mod perp_consume_events;
mod perp_create_market;
mod perp_edit_market;
mod perp_place_order;
mod perp_settle_fees;
mod perp_settle_pnl;
mod perp_update_funding;
mod serum3_cancel_all_orders;

View File

@ -91,6 +91,7 @@ pub fn perp_create_market(
open_interest: 0,
seq_num: 0,
fees_accrued: I80F48::ZERO,
fees_settled: I80F48::ZERO,
// Why optional - Perp could be based purely on an oracle
bump: *ctx.bumps.get("perp_market").ok_or(MangoError::SomeError)?,
base_token_decimals,

View File

@ -0,0 +1,103 @@
use anchor_lang::prelude::*;
use checked_math as cm;
use fixed::types::I80F48;
use crate::accounts_zerocopy::*;
use crate::error::*;
use crate::state::compute_health;
use crate::state::new_fixed_order_account_retriever;
use crate::state::Bank;
use crate::state::HealthType;
use crate::state::MangoAccount;
use crate::state::QUOTE_TOKEN_INDEX;
use crate::state::{oracle_price, AccountLoaderDynamic, Group, PerpMarket};
#[derive(Accounts)]
pub struct PerpSettleFees<'info> {
pub group: AccountLoader<'info, Group>,
#[account(mut, has_one = group, has_one = oracle)]
pub perp_market: AccountLoader<'info, PerpMarket>,
// This account MUST have a loss
#[account(mut, has_one = group)]
pub account: AccountLoaderDynamic<'info, MangoAccount>,
pub oracle: UncheckedAccount<'info>,
#[account(mut, has_one = group)]
pub quote_bank: AccountLoader<'info, Bank>,
}
pub fn perp_settle_fees(ctx: Context<PerpSettleFees>, max_settle_amount: I80F48) -> Result<()> {
// max_settle_amount must greater than zero
require!(
max_settle_amount > 0,
MangoError::MaxSettleAmountMustBeGreaterThanZero
);
let mut account = ctx.accounts.account.load_mut()?;
let mut bank = ctx.accounts.quote_bank.load_mut()?;
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
// Verify that the bank is the quote currency bank
require!(
bank.token_index == QUOTE_TOKEN_INDEX,
MangoError::InvalidBank
);
// Get oracle price for market. Price is validated inside
let oracle_price = oracle_price(
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
perp_market.oracle_config.conf_filter,
perp_market.base_token_decimals,
)?;
// Fetch perp positions for accounts
let perp_position = account.perp_position_mut(perp_market.perp_market_index)?;
// Settle funding before settling any PnL
perp_position.settle_funding(&perp_market);
// Calculate PnL for each account
let base_native = perp_position.base_position_native(&perp_market);
let pnl: I80F48 = cm!(perp_position.quote_position_native + base_native * oracle_price);
// Account perp position must have a loss to be able to settle against the fee account
require!(pnl.is_negative(), MangoError::ProfitabilityMismatch);
require!(
perp_market.fees_accrued.is_positive(),
MangoError::ProfitabilityMismatch
);
// Settle for the maximum possible capped to max_settle_amount
let settlement = pnl
.abs()
.min(perp_market.fees_accrued.abs())
.min(max_settle_amount);
perp_position.quote_position_native = cm!(perp_position.quote_position_native + settlement);
perp_market.fees_accrued = cm!(perp_market.fees_accrued - settlement);
// Update the account's net_settled with the new PnL
let settlement_i64 = settlement.round().checked_to_num::<i64>().unwrap();
account.fixed.net_settled = cm!(account.fixed.net_settled - settlement_i64);
// Transfer token balances
// TODO: Need to guarantee that QUOTE_TOKEN_INDEX token exists at this point. I.E. create it when placing perp order.
let token_position = account.ensure_token_position(QUOTE_TOKEN_INDEX)?.0;
bank.withdraw_with_fee(token_position, settlement)?;
// Update the settled balance on the market itself
perp_market.fees_settled = cm!(perp_market.fees_settled + settlement);
// Bank & perp_market are dropped to prevent re-borrow from remaining_accounts
drop(bank);
drop(perp_market);
// Verify that the result of settling did not violate the health of the account that lost money
let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
let health = compute_health(&account.borrow(), HealthType::Init, &retriever)?;
require!(health >= 0, MangoError::HealthMustBePositive);
msg!("settled fees = {}", settlement);
Ok(())
}

View File

@ -498,6 +498,10 @@ pub mod mango_v4 {
pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>, max_settle_amount: I80F48) -> Result<()> {
instructions::perp_settle_pnl(ctx, max_settle_amount)
}
pub fn perp_settle_fees(ctx: Context<PerpSettleFees>, max_settle_amount: I80F48) -> Result<()> {
instructions::perp_settle_fees(ctx, max_settle_amount)
}
// TODO
// perp_force_cancel_order
@ -505,7 +509,7 @@ pub mod mango_v4 {
// liquidate_token_and_perp
// liquidate_perp_and_perp
// settle_* - settle_funds, settle_fees
// settle_* - settle_funds
// resolve_banktruptcy

View File

@ -420,6 +420,7 @@ mod tests {
open_interest: 0,
seq_num: 0,
fees_accrued: I80F48::ZERO,
fees_settled: I80F48::ZERO,
bump: 0,
base_token_decimals: 0,
reserved: [0; 128],

View File

@ -75,6 +75,9 @@ pub struct PerpMarket {
/// Fees accrued in native quote currency
pub fees_accrued: I80F48,
/// Fees settled in native quote currency
pub fees_settled: I80F48,
/// Liquidity mining metadata
/// pub liquidity_mining_info: LiquidityMiningInfo,
@ -95,7 +98,7 @@ pub struct PerpMarket {
const_assert_eq!(
size_of::<PerpMarket>(),
32 + 2 + 2 + 4 + 16 + 32 + 16 + 32 * 3 + 8 * 2 + 16 * 11 + 8 * 2 + 8 * 2 + 16 + 2 + 6 + 8 + 128
32 + 2 + 2 + 4 + 16 + 32 + 16 + 32 * 3 + 8 * 2 + 16 * 12 + 8 * 2 + 8 * 2 + 16 + 2 + 6 + 8 + 128
);
const_assert_eq!(size_of::<PerpMarket>() % 8, 0);

View File

@ -2536,6 +2536,59 @@ impl ClientInstruction for PerpSettlePnlInstruction {
}
}
pub struct PerpSettleFeesInstruction {
pub group: Pubkey,
pub account: Pubkey,
pub perp_market: Pubkey,
pub oracle: Pubkey,
pub quote_bank: Pubkey,
pub max_settle_amount: I80F48,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for PerpSettleFeesInstruction {
type Accounts = mango_v4::accounts::PerpSettleFees;
type Instruction = mango_v4::instruction::PerpSettleFees;
async fn to_instruction(
&self,
account_loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let instruction = Self::Instruction {
max_settle_amount: self.max_settle_amount,
};
let accounts = Self::Accounts {
group: self.group,
perp_market: self.perp_market,
account: self.account,
oracle: self.oracle,
quote_bank: self.quote_bank,
};
let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
let account = account_loader
.load_mango_account(&self.account)
.await
.unwrap();
let health_check_metas = derive_health_check_remaining_account_metas(
&account_loader,
&account,
None,
false,
Some(perp_market.perp_market_index),
)
.await;
let mut instruction = make_instruction(program_id, &accounts, instruction);
instruction.accounts.extend(health_check_metas);
(accounts, instruction)
}
fn signers(&self) -> Vec<&Keypair> {
vec![]
}
}
pub struct BenchmarkInstruction {}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for BenchmarkInstruction {

View File

@ -1,9 +1,14 @@
#![allow(dead_code)]
use bytemuck::{bytes_of, Contiguous};
use fixed::types::I80F48;
use mango_v4::state::{PerpMarket, PerpPosition};
use solana_program::instruction::InstructionError;
use solana_program::program_error::ProgramError;
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::Keypair;
use solana_sdk::transaction::TransactionError;
use solana_sdk::transport::TransportError;
use std::ops::Deref;
pub fn gen_signer_seeds<'a>(nonce: &'a u64, acc_pk: &'a Pubkey) -> [&'a [u8]; 2] {
@ -75,3 +80,34 @@ impl From<Keypair> for TestKeypair {
Self(k)
}
}
pub fn get_pnl_native(
perp_position: &PerpPosition,
perp_market: &PerpMarket,
oracle_price: I80F48,
) -> I80F48 {
let contract_size = perp_market.base_lot_size;
let new_quote_pos =
I80F48::from_num(-perp_position.base_position_lots * contract_size) * oracle_price;
perp_position.quote_position_native - new_quote_pos
}
pub fn assert_mango_error<T>(
result: &Result<T, TransportError>,
expected_error: u32,
comment: String,
) {
match result {
Ok(_) => assert!(false, "No error returned"),
Err(TransportError::TransactionError(tx_err)) => match tx_err {
TransactionError::InstructionError(_, err) => match err {
InstructionError::Custom(err_num) => {
assert_eq!(*err_num, expected_error, "{}", comment);
}
_ => assert!(false, "Not a mango error"),
},
_ => assert!(false, "Not a mango error"),
},
_ => assert!(false, "Not a mango error"),
}
}

View File

@ -754,30 +754,3 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
Ok(())
}
fn get_pnl_native(
perp_position: &PerpPosition,
perp_market: &PerpMarket,
oracle_price: I80F48,
) -> I80F48 {
let contract_size = perp_market.base_lot_size;
let new_quote_pos =
I80F48::from_num(-perp_position.base_position_lots * contract_size) * oracle_price;
perp_position.quote_position_native - new_quote_pos
}
fn assert_mango_error<T>(result: &Result<T, TransportError>, expected_error: u32, comment: String) {
match result {
Ok(_) => assert!(false, "No error returned"),
Err(TransportError::TransactionError(tx_err)) => match tx_err {
TransactionError::InstructionError(_, err) => match err {
InstructionError::Custom(err_num) => {
assert_eq!(*err_num, expected_error, "{}", comment);
}
_ => assert!(false, "Not a mango error"),
},
_ => assert!(false, "Not a mango error"),
},
_ => assert!(false, "Not a mango error"),
}
}

View File

@ -0,0 +1,623 @@
#![cfg(all(feature = "test-bpf"))]
use anchor_lang::prelude::ErrorCode;
use fixed::types::I80F48;
use mango_v4::{error::MangoError, state::*};
use program_test::*;
use solana_program_test::*;
use solana_sdk::{signature::Keypair, transport::TransportError};
mod program_test;
#[tokio::test]
async fn test_perp_settle_fees() -> Result<(), TransportError> {
let context = TestContext::new().await;
let solana = &context.solana.clone();
let admin = &Keypair::new();
let owner = &context.users[0].key;
let payer = &context.users[1].key;
let mints = &context.mints[0..2];
let payer_mint_accounts = &context.users[1].token_accounts[0..=2];
let initial_token_deposit = 10_000;
//
// SETUP: Create a group and an account
//
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
admin,
payer,
mints,
}
.create(solana)
.await;
let account_0 = send_tx(
solana,
AccountCreateInstruction {
account_num: 0,
token_count: 16,
serum3_count: 8,
perp_count: 8,
perp_oo_count: 8,
group,
owner,
payer,
},
)
.await
.unwrap()
.account;
let account_1 = send_tx(
solana,
AccountCreateInstruction {
account_num: 1,
token_count: 16,
serum3_count: 8,
perp_count: 8,
perp_oo_count: 8,
group,
owner,
payer,
},
)
.await
.unwrap()
.account;
//
// SETUP: Deposit user funds
//
{
let deposit_amount = initial_token_deposit;
send_tx(
solana,
TokenDepositInstruction {
amount: deposit_amount,
account: account_0,
token_account: payer_mint_accounts[0],
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
send_tx(
solana,
TokenDepositInstruction {
amount: deposit_amount,
account: account_0,
token_account: payer_mint_accounts[1],
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
}
{
let deposit_amount = initial_token_deposit;
send_tx(
solana,
TokenDepositInstruction {
amount: deposit_amount,
account: account_1,
token_account: payer_mint_accounts[0],
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
send_tx(
solana,
TokenDepositInstruction {
amount: deposit_amount,
account: account_1,
token_account: payer_mint_accounts[1],
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
}
//
// TEST: Create a perp market
//
let mango_v4::accounts::PerpCreateMarket {
perp_market,
asks,
bids,
event_queue,
..
} = send_tx(
solana,
PerpCreateMarketInstruction {
group,
admin,
oracle: tokens[0].oracle,
asks: context
.solana
.create_account_for_type::<BookSide>(&mango_v4::id())
.await,
bids: context
.solana
.create_account_for_type::<BookSide>(&mango_v4::id())
.await,
event_queue: {
context
.solana
.create_account_for_type::<EventQueue>(&mango_v4::id())
.await
},
payer,
perp_market_index: 0,
base_token_index: tokens[0].index,
base_token_decimals: tokens[0].mint.decimals,
quote_lot_size: 10,
base_lot_size: 100,
maint_asset_weight: 0.975,
init_asset_weight: 0.95,
maint_liab_weight: 1.025,
init_liab_weight: 1.05,
liquidation_fee: 0.012,
maker_fee: 0.0002,
taker_fee: 0.000,
},
)
.await
.unwrap();
//
// TEST: Create another perp market
//
let mango_v4::accounts::PerpCreateMarket {
perp_market: perp_market_2,
..
} = send_tx(
solana,
PerpCreateMarketInstruction {
group,
admin,
oracle: tokens[1].oracle,
asks: context
.solana
.create_account_for_type::<BookSide>(&mango_v4::id())
.await,
bids: context
.solana
.create_account_for_type::<BookSide>(&mango_v4::id())
.await,
event_queue: {
context
.solana
.create_account_for_type::<EventQueue>(&mango_v4::id())
.await
},
payer,
perp_market_index: 1,
base_token_index: tokens[1].index,
base_token_decimals: tokens[1].mint.decimals,
quote_lot_size: 10,
base_lot_size: 100,
maint_asset_weight: 0.975,
init_asset_weight: 0.95,
maint_liab_weight: 1.025,
init_liab_weight: 1.05,
liquidation_fee: 0.012,
maker_fee: 0.0002,
taker_fee: 0.000,
},
)
.await
.unwrap();
let price_lots = {
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
perp_market.native_price_to_lot(I80F48::from(1000))
};
// Set the initial oracle price
send_tx(
solana,
StubOracleSetInstruction {
group,
admin,
mint: mints[0].pubkey,
payer,
price: "1000.0",
},
)
.await
.unwrap();
//
// Place orders and create a position
//
send_tx(
solana,
PerpPlaceOrderInstruction {
group,
account: account_0,
perp_market,
asks,
bids,
event_queue,
oracle: tokens[0].oracle,
owner,
side: Side::Bid,
price_lots,
max_base_lots: 1,
max_quote_lots: i64::MAX,
client_order_id: 0,
},
)
.await
.unwrap();
send_tx(
solana,
PerpPlaceOrderInstruction {
group,
account: account_1,
perp_market,
asks,
bids,
event_queue,
oracle: tokens[0].oracle,
owner,
side: Side::Ask,
price_lots,
max_base_lots: 1,
max_quote_lots: i64::MAX,
client_order_id: 0,
},
)
.await
.unwrap();
send_tx(
solana,
PerpConsumeEventsInstruction {
group,
perp_market,
event_queue,
mango_accounts: vec![account_0, account_1],
},
)
.await
.unwrap();
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
assert_eq!(mango_account_0.perps[0].base_position_lots, 1);
assert_eq!(
mango_account_0.perps[0].quote_position_native.round(),
-100_020
);
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(mango_account_1.perps[0].base_position_lots, -1);
assert_eq!(mango_account_1.perps[0].quote_position_native, 100_000);
// Bank must be valid for quote currency
let result = send_tx(
solana,
PerpSettleFeesInstruction {
group,
account: account_0,
perp_market,
oracle: tokens[0].oracle,
quote_bank: tokens[1].bank,
max_settle_amount: I80F48::MAX,
},
)
.await;
assert_mango_error(
&result,
MangoError::InvalidBank.into(),
"Bank must be valid for quote currency".to_string(),
);
// Oracle must be valid for the perp market
let result = send_tx(
solana,
PerpSettleFeesInstruction {
group,
account: account_0,
perp_market,
oracle: tokens[1].oracle, // Using oracle for token 1 not 0
quote_bank: tokens[0].bank,
max_settle_amount: I80F48::MAX,
},
)
.await;
assert_mango_error(
&result,
ErrorCode::ConstraintHasOne.into(),
"Oracle must be valid for perp market".to_string(),
);
// Cannot settle position that does not exist
let result = send_tx(
solana,
PerpSettleFeesInstruction {
group,
account: account_1,
perp_market: perp_market_2,
oracle: tokens[1].oracle,
quote_bank: tokens[0].bank,
max_settle_amount: I80F48::MAX,
},
)
.await;
assert_mango_error(
&result,
MangoError::PerpPositionDoesNotExist.into(),
"Cannot settle a position that does not exist".to_string(),
);
// max_settle_amount must be greater than zero
for max_amnt in vec![I80F48::ZERO, I80F48::from(-100)] {
let result = send_tx(
solana,
PerpSettleFeesInstruction {
group,
account: account_1,
perp_market: perp_market,
oracle: tokens[0].oracle,
quote_bank: tokens[0].bank,
max_settle_amount: max_amnt,
},
)
.await;
assert_mango_error(
&result,
MangoError::MaxSettleAmountMustBeGreaterThanZero.into(),
"max_settle_amount must be greater than zero".to_string(),
);
}
// TODO: Test funding settlement
{
let bank = solana.get_account::<Bank>(tokens[0].bank).await;
assert_eq!(
mango_account_0.tokens[0].native(&bank).round(),
initial_token_deposit,
"account 0 has expected amount of tokens"
);
assert_eq!(
mango_account_1.tokens[0].native(&bank).round(),
initial_token_deposit,
"account 1 has expected amount of tokens"
);
}
// Try and settle with high price
send_tx(
solana,
StubOracleSetInstruction {
group,
admin,
mint: mints[0].pubkey,
payer,
price: "1200.0",
},
)
.await
.unwrap();
// Account must have a loss
let result = send_tx(
solana,
PerpSettleFeesInstruction {
group,
account: account_0,
perp_market,
oracle: tokens[0].oracle,
quote_bank: tokens[0].bank,
max_settle_amount: I80F48::MAX,
},
)
.await;
assert_mango_error(
&result,
MangoError::ProfitabilityMismatch.into(),
"Account must be unprofitable".to_string(),
);
// TODO: Difficult to test health due to fees being so small. Need alternative
// let result = send_tx(
// solana,
// PerpSettleFeesInstruction {
// group,
// account: account_1,
// perp_market,
// oracle: tokens[0].oracle,
// quote_bank: tokens[0].bank,
// max_settle_amount: I80F48::MAX,
// },
// )
// .await;
// assert_mango_error(
// &result,
// MangoError::HealthMustBePositive.into(),
// "Health of losing account must be positive to settle".to_string(),
// );
// Change the oracle to a more reasonable price
send_tx(
solana,
StubOracleSetInstruction {
group,
admin,
mint: mints[0].pubkey,
payer,
price: "1005.0",
},
)
.await
.unwrap();
let expected_pnl_0 = I80F48::from(480); // Less due to fees
let expected_pnl_1 = I80F48::from(-500);
{
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
assert_eq!(
get_pnl_native(&mango_account_0.perps[0], &perp_market, I80F48::from(1005)).round(),
expected_pnl_0
);
assert_eq!(
get_pnl_native(&mango_account_1.perps[0], &perp_market, I80F48::from(1005)),
expected_pnl_1
);
}
solana.advance_clock().await;
// Check the fees accrued
let initial_fees = I80F48::from(20);
{
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
assert_eq!(
perp_market.fees_accrued.round(),
initial_fees,
"Fees from trading have been accrued"
);
assert_eq!(
perp_market.fees_settled.round(),
0,
"No fees have been settled yet"
);
}
// Partially execute the settle
let partial_settle_amount = I80F48::from(10);
send_tx(
solana,
PerpSettleFeesInstruction {
group,
account: account_1,
perp_market,
oracle: tokens[0].oracle,
quote_bank: tokens[0].bank,
max_settle_amount: partial_settle_amount,
},
)
.await
.unwrap();
{
let bank = solana.get_account::<Bank>(tokens[0].bank).await;
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
assert_eq!(
mango_account_1.perps[0].base_position_lots, -1,
"base position unchanged for account 1"
);
assert_eq!(
mango_account_1.perps[0].quote_position_native.round(),
I80F48::from(100_000) + partial_settle_amount,
"quote position increased for losing position by fee settle amount"
);
assert_eq!(
mango_account_1.tokens[0].native(&bank).round(),
I80F48::from(initial_token_deposit) - partial_settle_amount,
"account 1 token native position decreased (loss) by max_settle_amount"
);
assert_eq!(
mango_account_1.net_settled, -partial_settle_amount,
"net_settled on account 1 updated with loss from settlement"
);
assert_eq!(
perp_market.fees_accrued.round(),
initial_fees - partial_settle_amount,
"Fees accrued have been reduced by partial settle"
);
assert_eq!(
perp_market.fees_settled.round(),
partial_settle_amount,
"Fees have been partially settled"
);
}
solana.advance_clock().await;
// Fully execute the settle
send_tx(
solana,
PerpSettleFeesInstruction {
group,
account: account_1,
perp_market,
oracle: tokens[0].oracle,
quote_bank: tokens[0].bank,
max_settle_amount: I80F48::MAX,
},
)
.await
.unwrap();
{
let bank = solana.get_account::<Bank>(tokens[0].bank).await;
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
assert_eq!(
mango_account_1.perps[0].base_position_lots, -1,
"base position unchanged for account 1"
);
assert_eq!(
mango_account_1.perps[0].quote_position_native.round(),
I80F48::from(100_000) + initial_fees,
"quote position increased for losing position by fees settled"
);
assert_eq!(
mango_account_1.tokens[0].native(&bank).round(),
I80F48::from(initial_token_deposit) - initial_fees,
"account 1 token native position decreased (loss)"
);
assert_eq!(
mango_account_1.net_settled, -initial_fees,
"net_settled on account 1 updated with loss from settlement"
);
assert_eq!(
perp_market.fees_accrued.round(),
0,
"Fees accrued have been reduced to zero"
);
assert_eq!(
perp_market.fees_settled.round(),
initial_fees,
"Fees have been fully settled"
);
}
Ok(())
}