Farseen0/scar-pairs huggingface dataset:
HIGH severity: Multiple inconsistencies in `sell-market-seat`. **Description:** The [sell_market_seat](https://github.com/deriverse/protocol-v1/blob/4f7bc8ac68325aa93b339ff91c0ac794ea17ffd9/src/program/processor/sell_market_seat.rs#L89) contains two inconsistencies when compared to other perp functions in the codebase:

Issue 1: stale underlying price used...

Vulnerable code:
// sell_market_seat.rs
let mut engine = PerpEngine::new(
    &ctx,
    signer,
    system_program,
    community_state.perp_fee_rate(),
    community_state.margin_call_penalty_rate(),
    community_state.spot_pool_ratio(),
)?;

let instrument: &mut InstrAccountHeader = InstrAccountHeader::from_account_info(
    ctx.instr_acc,
    program_id,
    Some(data.instr_id),
    root_state.version,
)?;

// @audit change_funding_rate() called WITHOUT set_underlying_px() first, stale price is being used
engine.change_funding_rate();

engine.change_funding_rate();  // Updates global funding rate

let info = client_state.perp_info()?;

//@audit check_funding_rate(client_state.temp_client_id) is NOT called
// Pending funding is NOT applied to client's funds

if info.perps != 0 || info.in_orders_funds != 0 || info.in_orders_perps != 0 {
    bail!(ImpossibleToClosePerpPosition);
}

// Using stale funds value that doesn't include pending funding
let collactable_losses = info.funds().min(client_state.perp_info4()?.soc_loss_funds);
engine.state.header.perp_insurance_fund += collactable_losses;

// Client receives funds without pending funding being applied
client_state.add_crncy_tokens((info.funds() - collactable_losses).max(0))?;
HIGH severity: Improper header handling in `SpotFeesReport` logging causes DoS on swap instruction. **Description:** In `match_orders`, `ref_payment` handles `client.header` being None by defaulting to 0, but `ref_client_id` uses `ok_or` and returns an error if `header` is None. This causes the function to fail when `total_fees` > 0 and `header` is None.

This will always happen when the user is.....

Vulnerable code:
solana_program::log::sol_log_data(&[bytemuck::bytes_of::<SpotFeesReport>(
                &SpotFeesReport {
                    tag: log_type::SPOT_FEES,
                    fees: total_fees,
                    ref_payment,
                    ref_client_id: client
                        .header
                        .as_ref()
                        .ok_or(drv_err!(DeriverseErrorKind::ClientPrimaryAccountMustBeSome))?
                        .ref_client_id,
                    ..SpotFeesReport::zeroed()
                },
            )]);
HIGH severity: Missing Signer Verification in `voting_reset` Function Allows Unauthorized Execution. **Description:** The `voting_reset` function in `src/program/processor/voting_reset.rs` only verifies that the `admin` account's public key matches the operator address, but does not verify that the `admin` account is actually a signer of the transaction. **This allows an attacker to execute the...

Vulnerable code:
let root_state: &RootState = RootState::from_account_info(root_acc, program_id)?;

    if root_state.operator_address != *admin.key {
        bail!(InvalidAdminAccount {
            expected_address: root_state.operator_address,
            actual_address: *admin.key
        })
    }

if !admin.is_signer {
        bail!(MustBeSigner {
            address: *admin.key
        })
    }
HIGH severity: Spot price manipulation can lead to unfair liquidations. **Description:** When perpetual instruments are configured without an oracle feed, the system uses the spot market's `last_px` as the `perp_underlying_px` for liquidation calculations. The spot price (`last_px`) can be manipulated through order book orders or AMM trades, allowing attackers to...

Vulnerable code:
pub fn check_long_margin_call(&mut self) -> Result<i64, DeriverseError> {
    let margin_call_px = (self.state.header.perp_underlying_px as f64
        * (1.0 - self.state.header.liquidation_threshold)) as i64;

    // If edge_px > margin_call_px, position gets liquidated
    // ...
}

pub fn check_short_margin_call(&mut self) -> Result<i64, DeriverseError> {
    let margin_call_px = (self.state.header.perp_underlying_px as f64
        * (1.0 + self.state.header.liquidation_threshold)) as i64;

    // If edge_px < margin_call_px, position gets liquidated
    // ...
}
MEDIUM severity: Fixed token account size causes initialization failures for token accounts whose mints have token 22 extensions are active. **Description:**  The `create_token` function in `token.rs` uses a hardcoded size of 165 bytes when creating SPL token accounts, regardless of whether the mint has Token 2022 extensions enabled. This will cause initialisation failures for mints with extensions that require additional account...

Vulnerable code:
let spl_lamports = rent.minimum_balance(165);
invoke(
    &system_instruction::create_account(
        creator.key,
        program_acc.key,
        spl_lamports,
        165, // @audit why is it hardcoded to 165?
        token_program_id.key,
    ),
    &[creator.clone(), program_acc.clone()],
)

let account_size = if token_program == TokenProgram::Token2022 {
    use spl_token_2022::extension::StateWithExtensions;
    StateWithExtensions::<spl_token_2022::state::Account>::try_get_account_len(mint)?
} else {
    165 // Standard SPL token account size
};

let spl_lamports = rent.minimum_balance(account_size);
invoke(
    &system_instruction::create_account(
        creator.key,
        program_acc.key,
        spl_lamports,
        account_size as u64,
        token_program_id.key,
    ),
    &[creator.clone(), program_acc.clone()],
)
MEDIUM severity: Accounts may be created with incorrect rent-exemption due to `Rent::default` usage. **Description:** This dos vulnerability arises from the use of hardcoded `rent` parameters during account creation, leading to underfunded accounts that fail rent exemption requirements.

The flawed implementation relies on `Rent::default` instead of the on-chain `Rent sysvar` for rent...

Vulnerable code:
let rent = &Rent::default();
    ...
    let spl_lamports: u64 = rent.minimum_balance(165);
MEDIUM severity: `change_points_program_expiration` is permissionless. **Description:** The `change_points_program_expiration` function fails to verify that the `admin` account has actually signed the transaction. While the function checks that the provided admin account key matches the expected `operator_address` in the root state, it does not verify that the...

Vulnerable code:
// Only checks that the admin key matches, but not checking if the admin has actually signed the transaction;
if root_state.operator_address != *admin.key {
    bail!(InvalidAdminAccount {
        expected_address: root_state.operator_address,
        actual_address: *admin.key,
    });
}
// Missing: if !admin.is_signer { ... }

if !admin.is_signer {
        bail!(InvalidAdminAccount {
            expected_address: root_state.operator_address,
            actual_address: *admin.key,
        });
    }
MEDIUM severity: Anyone can sell anyone else's market seat. **Description:** In the `sell_market_seat` function the `check_signer` parameter is incorrectly set to false, allowing any user to force-sell another user's market seat without proper authorization.

`new_for_perp` calls `ClientPrimaryAccountHeader::from_account_info_unchecked` since we have set...

Vulnerable code:
let mut client_state = ClientPrimaryState::new_for_perp(
    program_id,
    client_primary_acc,
    &ctx,
    signer,
    system_program,
    0,
    false,  // alloc = false (correct)
    false,  // @audit why false? can I supply others's
)?;

// ...existing code...
let mut client_state = ClientPrimaryState::new_for_perp(
    program_id,
    client_primary_acc,
    &ctx,
    signer,
    system_program,
    0,
    false,
    true,   // check_signer = true
)?;
// ...existing code...
MEDIUM severity: Voting is allowed even after voting period's end time.. **Description:** The `voting()` function does not validate whether the voting period has ended before processing and recording votes. The function only checks the voting end time in the `finalize_voting()` call at the very end, but by that point the user's vote has already been cast and included...

Vulnerable code:
//here vote has been included even if end time has been passed of this period
        match data.choice {
            VoteOption::DECREMENT => {
                community_account_header.voting_decr += voting_tokens;
            }
            VoteOption::INCREMENT => {
                community_account_header.voting_incr += voting_tokens;
            }
            _ => community_account_header.voting_unchange += voting_tokens,
        }
....
// the call to finalize voting is made later, so the late user's vote has been included
        community_state.finalize_voting(time, clock.slot as u32)?;
MEDIUM severity: Missing Slippage Protection in Market Seat Buy/Sell Operations. **Description:** The `buy_market_seat()` and `sell_market_seat()` functions calculate seat prices dynamically based on the current `perp_clients_count` at execution time, but provide no slippage protection. Users cannot specify maximum/minimum acceptable prices, exposing them to unexpected price...

Vulnerable code:
let seat_price = PerpEngine::get_place_buy_price(
    instrument.perp_clients_count,
    instrument.crncy_token_decs_count,
)?;

instrument.seats_reserve += seat_price;
let price = data.amount + seat_price;
// ... price is deducted without validation
client_state.sub_crncy_tokens(price)?;

let seat_price = PerpEngine::get_place_sell_price(
    instrument.perp_clients_count,
    instrument.crncy_token_decs_count,
)?;

client_state.add_crncy_tokens(seat_price)?;

pub fn get_place_buy_price(supply: u32, dec_factor: u32) -> Result<i64, DeriverseError> {
    let df = get_dec_factor(dec_factor);
    Ok(get_reserve(supply + 1, df)? - get_reserve(supply, df)?)
}

pub fn get_place_sell_price(supply: u32, dec_factor: u32) -> Result<i64, DeriverseError> {
    let df = get_dec_factor(dec_factor);
    Ok(get_reserve(supply, df)? - get_reserve(supply - 1, df)?)
}
MEDIUM severity: Missing Quorum Requirement in Governance Voting. **Description:** The governance voting system in `finalize_voting()` lacks a quorum requirement, allowing protocol parameters to be changed based on relative vote counts without validating that a minimum percentage of total token supply has participated. This enables a small minority of token...

Vulnerable code:
let decr = community_account_header.voting_decr;
let incr = community_account_header.voting_incr;
let unchange = community_account_header.voting_unchange;
let tag = community_account_header.voting_counter % 6;

if decr > unchange && decr > incr {
    // Apply DECREMENT - no quorum check
    match tag {
        0 => { community_account_header.spot_fee_rate -= 1; }
        // ... other parameters
    }
} else if incr > unchange && incr > decr {
    // Apply INCREMENT - no quorum check
    match tag {
        0 => { community_account_header.spot_fee_rate += 1; }
        // ... other parameters
    }
}

community_account_header.voting_supply = community_account_header.drvs_tokens;

let total_votes = decr + incr + unchange;
        let voting_supply = community_account_header.voting_supply;

        // Add quorum check (e.g., require at least 5% participation)
        const MIN_QUORUM_PERCENTAGE: i64 = 5; // 5% of voting supply
        let min_quorum = (voting_supply * MIN_QUORUM_PERCENTAGE) / 100;
MEDIUM severity: Inconsistent Token Account Address Requirements Between Deposit and Withdraw. **Description:** The `deposit()` and `withdraw()` functions have inconsistent requirements regarding token account addresses:

- **`deposit()`**: Allows deposits from any token account owned by the signer, not requiring an Associated Token Account (ATA)
- **`withdraw()`**: Requires withdrawals to...

Vulnerable code:
// deposit.rs lines 145-154
let old_token = check_spl_token(
    client_associated_token_acc,
    program_token_acc,
    token_state,
    token_program_id,
    mint,
    signer.key,  // Only verifies owner is signer
    &pda,
    data.token_id,
)?;

if client_token != *mint.key {
            bail!(InvalidMintAccount {
                token_id: token_state.id,
                expected_address: *mint.key,
                actual_address: client_token,
            });
        }

        let client_token_owner = client_token_acc.data.borrow()[32..].as_ptr() as *const Pubkey;

        if *client_token_owner != *signer {
            bail!(DeriverseErrorKind::InvalidTokenOwner {
                token_id: token_state.id,
                address: *client_token_acc.key,
                expected_adderss: *signer,
                actual_address: *client_token_owner,
            });
        }

// withdraw.rs lines 127-138
let expected_address = get_associated_token_address_with_program_id(
    signer.key,
    mint_acc.key,
    token_program_id.key,
);
if expected_address != *client_associated_token_acc.key {
    bail!(InvalidAssociatedTokenAddress {
        token_id: token_state.id,
        expected_address: expected_address,
        actual_address: *client_associated_token_acc.key,
    });
}
MEDIUM severity: Silent Error Handling in `clean_generic` Introduces Multiple Risks. **Description:** The `clean_generic()` function in `ClientPrimaryState` implements silent error handling for account parsing using `if let Ok(header)`, which introduces three potential issues that can lead to incorrect state updates, account desynchronization, and masking of underlying instruction.....

Vulnerable code:
if let Ok(header) = header { // Silent failure handling
                counter += 2;
                let client_infos_acc = next_account_info!(accounts_iter)?;

                SpotTradeAccountHeader::<SPOT_CLIENT_INFOS>::validate(
                    client_infos_acc,
                    program_id,
                    Some(header.instr_id),
                    version,
                )?;

                self.client_infos_acc = client_infos_acc;
                self.maps_acc = maps_acc;
                self.resolve_instr(client_infos_acc, false)?;
                self.finalize_spot()?;
            }
        }
        Ok(())
    }
MEDIUM severity: Lack of slippage in `spot_lp` liquidity operations due to relying on the changing `header.crncy_tokens` and `header.asset_tokens`. **Description:** The `spot_lp` function lacks slippage protection when adding or removing liquidity.

In most cases, the function calculates the required asset and currency tokens based on the current pool state at execution time, **without allowing users to specify minimum output amounts or...

Vulnerable code:
trade_crncy_tokens = ((instr_state.header.crncy_tokens + instr_state.header.pool_fees)
    as f64
    * amount as f64
    / instr_state.header.ps as f64) as i64;
trade_asset_tokens = (instr_state.header.asset_tokens as f64 * amount as f64
    / instr_state.header.ps as f64) as i64;

self.change_tokens(traded_qty, side)?;
                self.change_mints(traded_mints, side)?;
                self.log_amm(traded_qty, traded_mints, side);
MEDIUM severity: Referral Incentives Disabled for All Legitimate Users During Any Liquidation. **Description:** Once the engine detects any instrument that requires liquidation (`is_long_margin_call` or `is_short_margin_call`), the `margin_call` flag is set to true for every subsequent `new_perp_order`. This flag is passed unchanged into `match_{ask,bid}_orders`, which disables referral...

Vulnerable code:
let margin_call = engine.is_long_margin_call() || engine.is_short_margin_call();
    if !margin_call {
        engine.state.header.perp_spot_price_for_withdrowal = engine.state.header.perp_underlying_px;
    }

if engine.cross(price, OrderSide::Ask) {
            (remaining_qty, _, ref_payment) = engine.match_ask_orders(
                Some(&mut client_community_state),
                &MatchOrdersStaticArgs {
                    price,
                    qty: data.amount,
                    ref_discount,
                    ref_ratio: header.ref_program_ratio,
                    ref_expiration: header.ref_program_expiration,
                    ref_client_id: header.ref_client_id,
                    trades_limit: 0,
                    margin_call,
                    client_id: client_state.temp_client_id,
                },
            )?;
        }

let ref_payment = if self.time < args.ref_expiration && !args.margin_call {
            ((fees - rebates) as f64 * args.ref_ratio) as i64
        } else {
            0
        };
MEDIUM severity: Slippage Guard Could be Too Loose For Leveraged Perp Markets. **Description:** Perpetual “market” orders fall back to the same hard-coded `±12.5 %` price cap that is used for spot orders. While that cap is arguably acceptable for spot, it is dangerously loose for leveraged perp trading: a user can be filled up to 12.5 % away from the reference price in one...

Vulnerable code:
let (price, min_tokens) = PerpParams::get_settings(
        if data.order_type == OrderType::Limit as u8 {
            data.price
        } else if buy {
            px + (px >> 3)
        } else {
            px - (px >> 3)
        },
        data.amount,
        if buy { OrderSide::Bid } else { OrderSide::Ask },
        px,
        data.ioc,
        engine.dc,
    )?;
MEDIUM severity: Operator precedence Issue during points program expiration validation in `change_points_program_expiration`. **Description:** In `change_points_program_expiration`, the validation for `new_expiration_time` uses `!data.new_expiration_time < root_state.points_program_expiration` instead of `!(data.new_expiration_time < root_state.points_program_expiration)`, causes unintended behavior.

This occurs because.....

Vulnerable code:
if !data.new_expiration_time < root_state.points_program_expiration {
    bail!(InvalidNewExpirationTime {
        program_name: "Points Program".to_string(),
        new_time: data.new_expiration_time,
        old_time: root_state.points_program_expiration
    });
}

(!data.new_expiration_time) < root_state.points_program_expiration

!(data.new_expiration_time < root_state.points_program_expiration)
MEDIUM severity: Fee Prepayment Locked Due to Asset Record Cleanup after `fees_deposit`. **Description:** When a user deposits fee prepayment via `fees_deposit`, if the deposit reduces crncy_tokens to zero(right after or later afterwards), the asset record is cleared by `clean_and_check_token_records()`. Later, when the user attempts to withdraw via `fees_withdraw`, the `resolve()`...

Vulnerable code:
client_community_state.data[crncy_index].fees_prepayment += data.amount;
    client_state.sub_crncy_tokens(data.amount)?;

pub fn clean_and_check_token_records(&mut self) -> DeriverseResult {
        for r in self.assets.iter_mut() {
            if r.asset_id != 0
                && (r.asset_id & 0xFF000000) != AssetType::SpotOrders as u32
                && (r.asset_id & 0xFF000000) != AssetType::Perp as u32
            {
                match r.value.cmp(&0) {
                    std::cmp::Ordering::Equal => r.asset_id = 0,
                    std::cmp::Ordering::Less => bail!(InsufficientFunds),
                    std::cmp::Ordering::Greater => {}
                }
            }
        }
        Ok(())
    }

client_community_state.update(&mut client_state, &mut community_state, None)?;
    client_state.resolve(AssetType::Token, data.token_id, TokenType::Crncy, false)?;
    let dec_factor = get_dec_factor(community_state.base_crncy[crncy_index].decs_count) as f64;
    let amount = (data.amount as f64 / dec_factor) as i64;
MEDIUM severity: Decimal Mismatch in Fee Prepayment Accounting Causes Incorrect Balance Tracking. **Description:** There is a critical decimal mismatch between how fee prepayment is stored and withdrawn. In `fees_deposit`, the `fees_prepayment` field stores the raw `data.amount` value (in token's native decimal units), but in `fees_withdraw`, it subtracts `data.amount / dec_factor` (in...

Vulnerable code:
let prepayment = data.amount as f64 / dec_factor;
...
client_community_state.data[crncy_index].fees_prepayment += data.amount;  // Stores raw value
...
client_state.sub_crncy_tokens(data.amount)?;

let amount = (data.amount as f64 / dec_factor) as i64;  // Divides by dec_factor
client_community_state.data[crncy_index].fees_prepayment -= amount;  // Subtracts divided value

solana_program::log::sol_log_data(&[bytemuck::bytes_of::<FeesWithdrawReport>(
        &FeesWithdrawReport {
            tag: log_type::FEES_WITHDRAW,
            client_id: client_state.id,
            token_id: data.token_id,
            amount: data.amount,
            time: clock.unix_timestamp as u32,
            ..FeesWithdrawReport::zeroed()
        },
    )]);
HIGH severity: Users can sell their market seat without paying loss coverage. **Description:** When users incur loss, its covered from available insurance funds and is stored and tracked inside `taker_info4.loss_coverage`, this is what user owes to protocol before he closes his position, because insurance funds have been used to cover up for these losses incurred by user....

Vulnerable code:
if (*self.perp_info3).bids_entry != NULL_ORDER
                || (*self.perp_info3).asks_entry != NULL_ORDER
                || self.current_instr_index >= self.assets.len()
            {
                bail!(ClientDataDestruction);
            }

if (*self.perp_info4).loss_coverage > 0 {
                let delta = (*self.perp_info4)
                    .loss_coverage
                    .min((*self.perp_info).funds);
                if delta > 0 {
                    engine.state.header.perp_insurance_fund += delta;
                    (*self.perp_info4).loss_coverage -= delta;
                    (*self.perp_info).funds -= delta;
                }
            }
HIGH severity: Users are getting back their `soc-loss-funds` while selling their market seat. **Description:** Inside `sell-market-seat` when [soc-loss-funds](https://github.com/deriverse/protocol-v1/blob/30b06d2da69e956c000120cdc15907b5f33088d7/src/program/processor/sell_market_seat.rs#L122) are negative, which means user has added and contributed to protocol's soc loss funds & beared...

Vulnerable code:
let collactable_losses = info.funds().min(client_state.perp_info4()?.soc_loss_funds);
    engine.state.header.perp_insurance_fund += collactable_losses;

    client_state.add_crncy_tokens((info.funds() - collactable_losses).max(0))?;
HIGH severity: Users can provide old price feeds to trade in their favor. **Description:** The function [set_underlying_px](https://github.com/deriverse/protocol-v1/blob/30b06d2da69e956c000120cdc15907b5f33088d7/src/state/instrument.rs#L224) is being called from many places in code, its setting the underlying price from user provided oracle feed if oracle is set....

Vulnerable code:
let feed_id = next_account_info!(accounts_iter)?;
            if self.header.feed_id == *feed_id.key {
                let oracle_px =
                    i64::from_le_bytes(feed_id.data.borrow()[73..81].try_into().unwrap());
                let oracle_exponent =
                    9 + i32::from_le_bytes(feed_id.data.borrow()[89..93].try_into().unwrap());
                let mut dc: i64 = 1;
HIGH severity: `perp_statistics_reset` can be used by users to skip `collactable-losses` while selling market seat. **Description:** Inside `perp-statistics-reset` we set [soc loss funds](https://github.com/deriverse/protocol-v1/blob/30b06d2da69e956c000120cdc15907b5f33088d7/src/program/processor/perp_statistics_reset.rs#L91) to zero without checking if user incurred losses.

if user would have incurred losses,...

Vulnerable code:
//inside perp-statistics-reset
    client_state.perp_info4()?.soc_loss_funds = 0;

//inside sell-market-seat
    let collactable_losses = info.funds().min(client_state.perp_info4()?.soc_loss_funds);
    engine.state.header.perp_insurance_fund += collactable_losses;

    client_state.add_crncy_tokens((info.funds() - collactable_losses).max(0))?;