Farseen0/scar-pairs huggingface dataset:
MEDIUM severity: Missing Array Synchronization in `dividends_claim` Prevents Users from Claiming Dividends for Newly Added Base Currencies. **Description:** The `dividends_claim` instruction does not synchronize `client_community_state.data` with` community_state.base_crncy` before processing dividends. If a new base currency is added to the community and a user hasn't performed any operations (like `deposit` or `trading`) that...

Vulnerable code:
pub fn update(
    &mut self,
    client_primary_state: &mut ClientPrimaryState<'a, 'info>,
    community_state: &mut CommunityState,
    payer: Option<&'a AccountInfo<'info>>,
) -> DeriverseResult {
    self.update_records(community_state, payer)?;  // Synchronizes arrays
    // ... rest of the logic
}

fn update_records(
    &mut self,
    community_state: &CommunityState,
    payer: Option<&'a AccountInfo<'info>>,
) -> DeriverseResult {
    // ... reallocates if needed ...
    self.header.count = community_state.header.count;
    // ... creates new data array ...
    for (d, b) in self.data.iter_mut().zip(community_state.base_crncy.iter()) {
        d.crncy_token_id = b.crncy_token_id;
        d.fees_ratio = 1.0;
    }
}

for (d, b) in client_community_state
    .data
    .iter_mut()
    .zip(community_state.base_crncy.iter_mut())
{
    // Process dividends...
}
MEDIUM severity: `min_qty` Bypass via `IOC` limit order. **Description:** Spot orders compute the minimum order quantity `min_qty` from the user‐supplied limit price. An attacker can set an arbitrarily large limit price to drive `min_qty` down to a tiny value while the actual execution price is still clamped near the mark price. This bypasses the...

Vulnerable code:
if ioc != 0 {
            if px < last_px - max_diff {
                ipx = last_px - max_diff;
            } else if px > last_px + max_diff {
                ipx = last_px + max_diff;
            } else {
                ipx = px;
            }
        } else if px < last_px - max_diff || px > last_px + max_diff {
            bail!(DeriverseErrorKind::InvalidPrice {
                price: px,
                min_price: last_px - max_diff,
                max_price: last_px + max_diff,
            });
        } else {
            ipx = px;
        }
...
        let min_tokens = min_qty(px, dc);
        // TODO remove
        if qty != 0 && qty.abs() < min_tokens {
            bail!(DeriverseErrorKind::InvalidQuantity {
                value: qty,
                min_value: min_tokens,
                max_value: SPOT_MAX_AMOUNT,
            });
        }
HIGH severity: Wrong accounting of fee in perp engine. **Description:** In the perp fee calculation logic within [perp_engine.rs](https://github.com/deriverse/protocol-v1/blob/30b06d2da69e956c000120cdc15907b5f33088d7/src/program/perp/perp_engine.rs#L1202), when a user has partial fee prepayment that doesn't fully cover the trade fee, the function...

Vulnerable code:
} else {
                        let discount_sum = (*fees_prepayment as f64 / args.fee_rate) as i64;
                        let extra_fee = ((traded_crncy - discount_sum) as f64
                            * (1.0 - args.ref_discount)
                            * self.fee_rate) as i64;
                        let fee = discount_sum + extra_fee; // here, discount_sum is trade value, not fee
                        taker_info
                            .sub_funds(extra_fee)
                            .map_err(|err| drv_err!(err))?;
                        *fees_prepayment = 0;
                        fee
                    }

} else {
    let discount_sum = (*fees_prepayment as f64 / args.fee_rate) as i64;
    let extra_fee = ((traded_crncy - discount_sum) as f64
        * (1.0 - args.ref_discount)
        * self.fee_rate) as i64;
    // FIX: Use `fees_prepayment` (actual fee amount), not discount_sum (trade value)
    let fee = (*fees_prepayment) + extra_fee;
    taker_info
        .sub_funds(extra_fee)
        .map_err(|err| drv_err!(err))?;
    *fees_prepayment = 0;
    fee
}
HIGH severity: User's chosen leverage is overwritten to `max-leverage` on every perp operation. **Description:** In `client_primary.rs]`, the [new_for_perp](https://github.com/deriverse/protocol-v1/blob/30b06d2da69e956c000120cdc15907b5f33088d7/src/state/client_primary.rs#L344) function unconditionally sets the user's leverage on every call, regardless of whether the `alloc` parameter is true.....

Vulnerable code:
let mut client_state = ClientPrimaryState::new_for_perp(
        program_id,
        client_primary_acc,
        &ctx,
        signer,
        system_program,
        0, //always passed as zero
        false,
        true,
    )?;

if leverage > 0 {
    //clear and set
    unsafe {
        (*state.perp_info2).mask &= 0xFFFFFF00;
        (*state.perp_info2).mask |= (leverage as u32).min(header.max_leverage as u32);
    }
} else {
    // When leverage == 0, sets to max_leverage (always by default)
    unsafe {
        (*state.perp_info2).mask &= 0xFFFFFF00;
        (*state.perp_info2).mask |= header.max_leverage as u32;
    }
}
Ok(state)

if leverage > 0 {
    // Only update leverage when explicitly provided
    unsafe {
        (*state.perp_info2).mask &= 0xFFFFFF00;
        (*state.perp_info2).mask |= (leverage as u32).min(header.max_leverage as u32);
    }
}
// When leverage == 0, keep existing leverage unchanged
MEDIUM severity: Redundant State Updates in `fill` Function Cause Issues. **Description:** The `fill` function contains **redundant state updates** to `last_asset_tokens` and `last_crncy_tokens` that are already properly handled by `write_last_tokens`. This redundancy causes data loss, incorrect accumulation, and state inconsistency when multiple orders are filled...

Vulnerable code:
self.state.header.last_asset_tokens = traded_qty;
self.state.header.last_crncy_tokens = traded_crncy;

engine.write_last_tokens(traded_qty, traded_sum, trades, px)?;

if self.state.header.slot == self.slot {
    self.state.header.last_crncy_tokens = self
        .state
        .header
        .last_crncy_tokens
        .checked_add(traded_crncy_tokens)
        .ok_or(drv_err!(DeriverseErrorKind::ArithmeticOverflow))?;
    self.state.header.last_asset_tokens = self
        .state
        .header
        .last_asset_tokens
        .checked_add(traded_asset_tokens)
        .ok_or(drv_err!(DeriverseErrorKind::ArithmeticOverflow))?;
} else {
    self.state.header.slot = self.slot;
    self.state.header.last_crncy_tokens = traded_crncy_tokens;
    self.state.header.last_asset_tokens = traded_asset_tokens;
}
MEDIUM severity: Missing Trade Count Updates in `reversed_swap` Function for Buy Orders. **Description:** The `reversed_swap` function used for `buy` orders in `swap` operations fails to properly track and return `trade` counts, leading to incorrect trade statistics being written to the state. This inconsistency with `match_orders` causes buy order trades to be recorded as 0,...

Vulnerable code:
if buy {
        if price > px || engine.cross(price, OrderSide::Ask) {
            let total_fees;
            let remaining_sum;
            let input_sum = (data.amount as f64 / (1.0 + fee_rate)) as i64;

            (remaining_sum, traded_qty, total_fees) = engine.reversed_swap(price, input_sum)?;

(remaining_sum, traded_qty, total_fees) = engine.reversed_swap(price, input_sum)?;

if traded_qty > 0 && traded_sum > 0 {
        let candles = uninit_candles.init(&engine)?;
        engine.write_candles(candles, traded_qty, traded_sum)?;
        engine.write_last_tokens(traded_qty, traded_sum, trades, px)?;
    } else {
        bail!(DeriverseErrorKind::FailedToSwap {
            price,
            side: if buy { OrderSide::Ask } else { OrderSide::Bid }
        });
    }
MEDIUM severity: Missing `change_funding_rate` Call After Price Update in `perp_mass_cancel` and `perp_order_cancel`. **Description:** In `perp_order_cancel` and `perp_mass_cancel functions`, the code updates the underlying price (`perp_underlying_px`) via `set_underlying_px` but fails to call `change_funding_rate` before invoking `check_rebalancing`(and potentially `mass_cancel`), which internally calls...

Vulnerable code:
pub fn change_funding_rate(&mut self) {
        let time_delta = self.time - self.state.header.perp_funding_rate_time;
        if time_delta > 0 && self.state.header.perp_price_delta != 0.0 {
            self.state.header.perp_funding_rate +=
                ((time_delta as f64) / DAY as f64) * self.state.header.perp_price_delta;
        }
        self.state.header.perp_price_delta =
            (self.market_px() - self.state.header.perp_underlying_px) as f64 * self.rdf;
        self.state.header.perp_funding_rate_time = self.time;
    }
    /*

pub fn check_funding_rate(&mut self, temp_client_id: ClientId) -> Result<bool, DeriverseError> {
        let info = unsafe { &mut *(self.client_infos.offset(*temp_client_id as isize)) };
        let info5 = unsafe { &mut *(self.client_infos5.offset(*temp_client_id as isize)) };
        let perps = info.total_perps();
        let mut change = false;
        if perps != 0 {
            if self.state.header.perp_funding_rate != info5.last_funding_rate {
                let funding_funds = -(perps as f64
                    * (self.state.header.perp_funding_rate - info5.last_funding_rate))
                    .round() as i64;

// perp_order_cancel
engine.state.set_underlying_px(accounts_iter)?;
// ... order cancellation logic ...
engine.check_rebalancing()?;  // Calls check_funding_rate() internally

// perp_mass_cancel
engine.state.set_underlying_px(accounts_iter)?;
engine.mass_cancel(client_state.temp_client_id)?;  // Calls check_funding_rate() at line 1888
// ... margin call checks ...
engine.check_rebalancing()?;  // Also calls check_funding_rate() at line 2363
MEDIUM severity: Incorrect Margin Call State Detection After Liquidation in `perp_withdraw`. **Description:** In perp_withdraw, the margin call state is incorrectly determined after executing liquidation checks. The function calls `check_long_margin_call()` and `check_short_margin_call()` first which may successfully liquidate positions, but then checks the `margin_call` state using...

Vulnerable code:
engine.check_long_margin_call()?;
engine.check_short_margin_call()?;
let margin_call = engine.is_long_margin_call() || engine.is_long_margin_call();

let (_, t, _) = self.match_bid_orders(
                        None,
                        &MatchOrdersStaticArgs {
                            price: margin_call_px,
                            qty: info.perps,
                            ref_discount: 0f64,
                            ref_ratio: 0f64,
                            ref_expiration: 0,
                            ref_client_id: ClientId(0),
                            client_id: temp_client_id,
                            trades_limit: MAX_MARGIN_CALL_TRADES - trades,
                            margin_call: true,
                        },
                    )?;
                    self.check_funding_rate(temp_client_id)?;
                    self.change_edge_px(temp_client_id); // <= here

if info2.px_node != NULL_NODE {
            if info2.mask & 0x80000000 == 0 {
                let node: NodePtr<i128> =
                    unsafe { NodePtr::get(self.long_px.entry, info2.px_node) };
                self.long_px.delete(node);
            } else {
                let node: NodePtr<i128> =
                    unsafe { NodePtr::get(self.short_px.entry, info2.px_node) };
                self.short_px.delete(node);
            }
        }
MEDIUM severity: Missing Update of `perp_spot_price_for_withdrowal` in `perp_withdraw` Function. **Description:** The `perp_withdraw` function fails to update the `perp_spot_price_for_withdrowal` field(including `perp_long_spot_price_for_withdrowal` and `perp_short_spot_price_for_withdrowal` in later commits) when there is no margin call, while other similar functions (`new_perp_order` and...

Vulnerable code:
if !long_margin_call {
        engine.state.perp_long_spot_price_for_withdrowal = engine.state.perp_underlying_px;
    }
    if !short_margin_call {
        engine.state.perp_short_spot_price_for_withdrowal = engine.state.perp_underlying_px;
    }

let margin_call = engine.is_long_margin_call() || engine.is_long_margin_call();
if !margin_call {
    engine.check_rebalancing()?;
}
MEDIUM severity: Margin call uses stale edge price. **Description:** The `check_long_margin_call` and `check_short_margin_call` functions determine whether positions should be liquidated based on edge prices retrieved from the price trees before applying funding rate updates and rebalancing. This causes positions to be liquidated using stale edge...

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

    loop {
        let root = self.long_px.get_root::<i128>();
        if root.is_null() || trades >= MAX_MARGIN_CALL_TRADES {
            break;
        }
        let node = root.max_node();
        let px = (node.key() >> 64) as i64;  // Gets stale edge price

        if px > margin_call_px {  // Decision made with stale price
            let temp_client_id = ClientId(node.link());

            // Funding rate checked AFTER liquidation decision
            self.check_funding_rate(temp_client_id)?;
            self.check_soc_loss(temp_client_id)?;

            // ... liquidation logic ...

            // Edge price updated AFTER liquidation
            self.change_edge_px(temp_client_id);
        }
    }
}
LOW severity: Expired Private Client Cannot Be Re-added to Queue. **Description:** The `new_private_client()` function incorrectly rejects re-adding a wallet whose previous entry has expired. The function checks for duplicate wallet addresses before validating expiration status, preventing expired records from being treated as vacant slots.

In the record...

Vulnerable code:
if record.wallet == *wallet.key && record.creation_time != 0 {
    return Err(AlreadyExists(index));
}
LOW severity: Account Count Validation Mismatch in `new_base_crncy` Instruction. **Description:** The `new_base_crncy` instruction has an inconsistency between the declared minimum account count and the actual number of accounts required.

The function comment and implementation both indicate that `9` accounts are needed, but `NewBaseCrncyInstruction::MIN_ACCOUNTS` is set to...

Vulnerable code:
pub fn new_base_crncy(program_id: &Pubkey, accounts: &[AccountInfo], _: &[u8]) -> DeriverseResult {
    // New Base Currency Instruction
    // 1 - Admin (Signer)
    // 2 - Root Account (Read Only)
    // 3 - Token Account
    // 4 - Program Token Account (Signer when creating a new token)
    // 5 - Deriverse Authority Account (Read Only)
    // 6 - Token Program ID (Read Only)
    // 7 - Mint Address (Read Only, when wSOL can not be old native mint)
    // 8 - System Program (Read Only)
    // 9 - Community Account

However, the validation check only requires 8 accounts:

**Impact:**
1. **Incorrect Validation**: The instruction accepts `8` accounts when it actually requires `9,` creating a mismatch between the validation and the actual account requirements.
2. **Late Failure**: Transactions with only `8` accounts will pass the initial check but fail later during execution.

**Recommended Mitigation:** Update `MIN_ACCOUNTS` to `9` to match the actual account requirements:
LOW severity: Inflexible Voting System Prevents Rapid Parameter Adjustments. **Description:** The governance voting system uses a rigid rotation mechanism where which parameter can be modified is determined by `voting_counter % 6`.

This creates a fixed 6-parameter rotation cycle where each parameter can only be voted on once every 6 voting periods. Combined with the...

Vulnerable code:
let tag = community_account_header.voting_counter % 6;
match tag {
    0 => spot_fee_rate,
    1 => perp_fee_rate,
    2 => spot_pool_ratio,
    3 => margin_call_penalty_rate,
    4 => fees_prepayment_for_max_discount,
    _ => max_discount,
}

if community_state.header.voting_counter == 0 {
    community_state.header.upgrade()?.voting_counter += 1;
}
community_state.finalize_voting(clock.unix_timestamp as u32, clock.slot as u32)?;

#[cfg(not(feature = "test-sbf"))]
pub fn voting_end(time: u32) -> u32 {
    let days = (time - SETTLEMENT) / DAY;
    days * DAY + 14 * DAY + SETTLEMENT
}
LOW severity: Dividend Calculation Uses Stale Token Balance in Subsequent `update()` Calls After `fees_deposit` with DRVS. **Description:** The `fees_deposit()` function calls `client_community_state.update()` before `client_state.sub_crncy_tokens()`, causing `self.header.drvs_tokens` to be updated with a stale value that doesn't reflect the actual token balance after the reduction. When `update()` is called again in...

Vulnerable code:
// src/program/processor/fees_deposit.rs
client_community_state.update(&mut client_state, &mut community_state)?;
client_state.resolve(AssetType::Token, data.token_id, TokenType::Crncy, false)?;
// ... fee calculation logic ...
client_state.sub_crncy_tokens(data.amount)?;

// src/state/client_community.rs
        if available_tokens != self.header.drvs_tokens {
            for (i, d) in self.data.iter_mut().enumerate() {
                let amount = (((community_state.base_crncy[i].rate - d.dividends_rate)
                    * self.header.drvs_tokens as f64) as i64)
                    .max(0);
                d.dividends_value += amount;
            }
LOW severity: Griefing Attack: Malicious Takers Can Force Order Cancellation by Partial Filling Below Minimum Quantity. **Description:** A malicious taker can exploit the automatic order cancellation mechanism to force makers' orders to be cancelled by intentionally partially filling orders such that the remaining quantity falls below the `min_qty` threshold.

When this occurs, the system automatically cancels the...

Vulnerable code:
// src/program/spot/engine.rs:1265-1279
if last {
    order.decr_qty(traded_qty).map_err(|err| drv_err!(err))?;
    order.decr_sum(traded_crncy).map_err(|err| drv_err!(err))?;
    order.set_time(self.time);
    if order.qty() < min_qty {  // ⚠️ check
        let node = self.get_node_ptr(order.link(), fill_static_args.side);
        self.erase_client_order(order, node, true, fill_static_args.side)?;
        // ... order is cancelled and funds refunded
    }
}
LOW severity: Missing `last_time` Update in `spot_lp` Causes Incorrect Daily Trade Statistics. **Description:** The `spot_lp` function uses `instr_state.header.last_time` to determine if a day has passed, but never updates this field.

In contrast, other trading functions (`swap, new_spot_order, spot_quotes_replace`) update `last_time` via `engine.write_last_tokens()`. This inconsistency...

Vulnerable code:
if instr_state.header.last_time < fixing_time {
    instr_state.header.lp_prev_day_trades = instr_state.header.lp_day_trades;
    instr_state.header.lp_day_trades = 1;
} else {
    instr_state.header.lp_day_trades += 1;
}

instr_state.header.lp_day_trades += 1;
}
instr_state.header.last_time = time;  // Add this line
LOW severity: Missing Signer and New Account Validation for `asset_token_program_acc` in `new_instrument`. **Description:** The `new_instrument` instruction lacks validation checks for `asset_token_program_acc` when creating a new asset token account. Unlike `new_base_crncy` which explicitly validates that the program token account is a signer and a new account, `new_instrument` omits these checks,...

Vulnerable code:
if is_new_account(asset_token_acc) {
        let decimals = *asset_mint
            .data
            .borrow()
            .get(MINT_DECIMALS_OFFSET)
            .ok_or_else(|| drv_err!(DeriverseErrorKind::InvalidClientDataFormat))?
            as u32;

        if !(MIN_DECS_COUNT..=MAX_DECS_COUNT).contains(&decimals) {
            bail!(DeriverseErrorKind::InvalidDecsCount {
                decs_count: decimals,
                min: MIN_DECS_COUNT,
                max: MAX_DECS_COUNT,
                token_address: *asset_mint.key,
            });
        }

        #[cfg(feature = "native_mint_2022")]
        if *asset_mint.key == spl_token::native_mint::ID {
            bail!(LegacyNativeMintNotSupported);
        }
        #[cfg(not(feature = "native_mint_2022"))]
        if *asset_mint.key == spl_token_2022::native_mint::ID {
            bail!(Token2022NativeMintNotSupported);
        }

        TokenState::create_token(
            root_state,
            asset_mint,
            asset_token_acc,
            drvs_auth_acc,
            &drvs_auth,
            bump_seed,
            program_id,
            asset_token_program_acc,
            token_program,
            signer,
            decimals,
        )?;

///
/// [*Incorrect Price Validation When Creating `NewInstrumentData` Struct during `NewInstrumentInstruction` instruction*](#incorrect-price-validation-when-creating-newinstrumentdata-struct-during-newinstrumentinstruction-instruction) - Asset Tokens Program Account `[SPL, if new_token signer]` - Spl token account
///

if is_new_account(token_acc) {
    if !is_new_account(program_acc) {
        bail!(InvalidNewAccount { ... });
    }
    if !program_acc.is_signer {
        return Err(drv_err!(MustBeSigner { ... }));
    }
    // ...
}
LOW severity: Inconsistent Price Calculation for Fee in Spot LP Trading. **Description:** The fee calculation in the `spot_lp` instruction uses `last_px` directly without applying the same price constraints (`best_bid, best_ask`) and unit conversion (`RDF`) that are used when adding liquidity. This inconsistency can lead to incorrect fee calculations when `last_px`...

Vulnerable code:
let px_f64 = instr_state
    .header
    .last_px
    .max(instr_state.header.best_bid)
    .min(instr_state.header.best_ask) as f64
    * RDF;

fees = 1
    + ((instr_state.header.last_px as f64
        / get_dec_factor(instr_state.header.asset_token_decs_count) as f64)
        as i64)
        .max(1);
HIGH severity: `get_by_tag` tries to access out of bound index. **Description:** The [get_by_tag](https://github.com/deriverse/protocol-v1/blob/30b06d2da69e956c000120cdc15907b5f33088d7/src/program/constants.rs#L4) contains an off-by-one error in its loop condition. The function iterates using [while i <=...

Vulnerable code:
pub const fn get_by_tag<const TAG: u32>(
    continer: CandleRegister,
) -> Result<CandleParams, DeriverseError> {
    let mut i = 0;

    while i <= continer.candles.len() {  // BUG: should be `<` not `<=`
        if continer.candles[i].tag == TAG {  // Out-of-bounds when i == len
            return Ok(continer.candles[i]);
        }
        i += 1;
    }
    // ...
}

Err(DeriverseError {
        error: DeriverseErrorKind::CandleWasNotFound { tag: TAG },
        location: ErrorLocation {
            file: file!(),
            line: line!(),
        },
    })

pub const fn get_by_tag<const TAG: u32>(
    continer: CandleRegister,
) -> Result<CandleParams, DeriverseError> {
    let mut i = 0;

    while i < continer.candles.len() {  // Fixed: use `<` instead of `<=`
        if continer.candles[i].tag == TAG {
            return Ok(continer.candles[i]);
        }
        i += 1;
    }

    Err(DeriverseError {
        error: DeriverseErrorKind::CandleWasNotFound { tag: TAG },
        location: ErrorLocation {
            file: file!(),
            line: line!(),
        },
    })
}