Farseen0/scar-pairs huggingface dataset:
MEDIUM severity: Seizing payouts for frozen users can lead to double spending if the holder is unfrozen in subsequent distributions. **Description:** The `ChildToken::seizeFrozenFunds` function is designed to perform the following operations:
- Permit the legitimate holder to claim all dividend distributions accrued prior to the imposition of the freeze.
- Transfer to the designated custodian all dividend distributions...

Vulnerable code:
function test_PoC_DoubleSpendingPayoutsOfFrozenHolder() public {
        // distribute payout, freeze holder, distribute more payouts while holder is frozen, seizeFrozen, distribute a payout, call to payoutBalance() to reset lastIndex to frozenIndex, then unfreeze holder, call payoutBalance() and get access to all payouts since the user was frozen!
        address user = domesticUsers[0];
        address custodian = domesticUsers[1];

        uint64 tokenToMint = 5;
        uint64 payoutAmount = 100e6;

        // mint and send user the tokens
        _mintAndTransferToUser(user, tokenToMint);

        // create distribution + verify
        _fundPayoutToPaymentSettler(payoutAmount);
        assertEq(d_childTokenProxy.payoutBalance(user), payoutAmount);

        assertEq(d_childTokenProxy.payoutBalance(user), payoutAmount);

        // freeze user + 5 payouts
        d_childTokenProxy.freezeHolder(user);
        _fundPayoutToPaymentSettler(payoutAmount);
        _fundPayoutToPaymentSettler(payoutAmount);
        _fundPayoutToPaymentSettler(payoutAmount);
        _fundPayoutToPaymentSettler(payoutAmount);
        _fundPayoutToPaymentSettler(payoutAmount);

        // seize frozen payouts from user, send frozen funds to custodian
        d_childTokenProxy.seizeFrozenFunds(user, custodian, false);
        // user receives the payouts owed prior to being frozen
        assertEq(stableCoin.balanceOf(user), payoutAmount);
        // custodian receives the payouts while the user was frozen
        assertEq(stableCoin.balanceOf(custodian), payoutAmount * 5);
        assertEq(d_childTokenProxy.payoutBalance(user), 0);
        assertEq(d_childTokenProxy.isHolderFrozen(user), true);

        // One more payout - Since the user was frozen, this is the 6th payout
        _fundPayoutToPaymentSettler(payoutAmount);

        assertEq(d_childTokenProxy.payoutBalance(user), 0);

        d_childTokenProxy.unFreezeHolder(user);

        //@audit-issue => Because `frozenIndex` was not updated, bug allows the user to claim the 6 payouts since he was frozen regardless that 5 of those 6 payouts have already been paid out to the custodian via the `seizeFrozenFunds()`
        assertEq(d_childTokenProxy.payoutBalance(user), payoutAmount * 6);

    }
HIGH severity: Owner can rescue the vault’s own share tokens. **Description:** [`SherpaVault::rescueTokens`](https://github.com/hedgemonyxyz/sherpa-vault-smartcontracts/blob/50eb8ad6ee048a767f7ed2265404c59592c098b7/contracts/SherpaVault.sol#L730-L740) forbids rescuing the wrapper token:

But it allows rescuing the vault’s own share token (`token ==...

Vulnerable code:
// CRITICAL: Cannot rescue the wrapper token (user funds)
// This protects deposited SherpaUSD from being withdrawn by owner
if (token == stableWrapper) revert CannotRescueWrapperToken();

accountingSupply += mintShares;
_mint(address(this), mintShares);

- if (token == stableWrapper) revert CannotRescueWrapperToken();
+ if (token == stableWrapper || token == address(this)) revert CannotRescueWrapperToken();
LOW severity: SherpaUSD does not work with fee-on-transfer tokens. **Description:** The SherpaUSD contract cannot work correctly with fee-on-transfer tokens. An example of such a token is USDT, which is expected to be supported as per comments. Note: Fees are not yet activated on USDT however they can be at any time in the future.

For example:
 - Assume 2% fees...

Vulnerable code:
// CRITICAL: SherpaUSD only supports 6-decimal assets (USDC, USDT, etc.)

function depositToVault(
    address from,
    uint256 amount
) external nonReentrant onlyKeeper {
    if (amount == 0) revert AmountMustBeGreaterThanZero();

    _mint(keeper, amount);
    depositAmountForEpoch += amount;

    emit DepositToVault(from, amount);

    IERC20(asset).safeTransferFrom(from, address(this), amount);
}
LOW severity: Direct amount assignment in `SherpaUSD::ownerMint`/`ownerBurn` can break accounting for totalStaked and accountingSupply. **Description:** Functions `SherpaUSD::ownerMint` and `ownerBurn` directly assign the amount parameter to mappings `approvedTotalStakedAdjustment` and `approvedAccountingAdjustment`. This will however not work correctly if more tokens are minted or burned to/from the vault before the approvals are.....

Vulnerable code:
function ownerMint(address to, uint256 amount) external onlyOperator {
    _mint(to, amount);

    // Approve vault to adjust by this amount
    approvedTotalStakedAdjustment[to] = amount;
    approvedAccountingAdjustment[to] = amount;

    emit PermissionedMint(to, amount);
    emit RebalanceApprovalSet(to, amount, amount);
}

/**
 * @notice Operator-level burn for manual rebalancing across chains
 * @param from Address to burn from
 * @param amount Amount to burn
 * @dev Sets approval for vault to adjust totalStaked and accountingSupply
 */
function ownerBurn(address from, uint256 amount) external onlyOperator {
    _burn(from, amount);

    // Approve vault to adjust by this amount
    approvedTotalStakedAdjustment[from] = amount;
    approvedAccountingAdjustment[from] = amount;

    emit PermissionedBurn(from, amount);
    emit RebalanceApprovalSet(from, amount, amount);
}
HIGH severity: `SherpaVault::_rollInternal` price calculation comment and math inconsistent. **Description:** When calculating a new price a script queries all vaults on all chains then passes that to `SherpaVault:: rollToNextRound`. This in turn calls [`SherpaVault::_rollInternal`](https://github.com/hedgemonyxyz/sherpa-vault-smartcontracts/blob/50eb8ad6ee048a767f7ed2265404c59592c098b7/con...

Vulnerable code:
// Calculate global price using script-provided totals
// globalBalance must include pending deposits for correct price calculation
uint256 globalBalance = isYieldPositive
    ? globalTotalStaked + globalTotalPending + yield
    : globalTotalStaked + globalTotalPending - yield;

uint256 newPricePerShare = ShareMath.pricePerShare(
    globalShareSupply,
    globalBalance,
    globalTotalPending,
    _vaultParams.decimals
);

function pricePerShare(
    uint256 totalSupply,   // @audit-info globalShareSupply
    uint256 totalBalance,  // @audit-info globalBalance
    uint256 pendingAmount, // @audit-info globalTotalPending
    uint256 decimals
) internal pure returns (uint256) {
    uint256 singleShare = 10 ** decimals;
    return
        totalSupply > 0
            ? (singleShare * (totalBalance - pendingAmount)) / totalSupply
            : singleShare;
}
HIGH severity: `SherpaUSD::consumeTotalStakedApproval` and `SherpaUSD::consumeAccountingApproval` callable by anyone. **Description:** In the rebalancing/settlement flow for cross-chain accounting, [`SherpaUSD::consumeTotalStakedApproval`](48a767f7ed2265404c59592c098b7/contracts/SherpaUSD.sol#L282-L291) and [`SherpaUSD::consumeAccountingApproval`](https://github.com/hedgemonyxyz/sherpa-vault-smartcontracts/blob/50e...

Vulnerable code:
function consumeTotalStakedApproval(address vault) external {
    // @audit-issue anyone can call by passing their own address as `vault`
    if (msg.sender != vault) revert OnlyVaultCanConsume();
    approvedTotalStakedAdjustment[vault] = 0;
    emit TotalStakedApprovalConsumed(vault);
}
LOW severity: `BLS12381::hashToG1` can use constants for DST. **Description:** In `BLS12381::hashToG1`, the Domain Separation Tag (DST) string is passed as an inline string literal to `expandMsg`, causing unnecessary memory allocation on every call.

https://github.com/symbioticfi/relay-contracts/blob/main/src/libraries/utils/BLS12381.sol#L287

At runtime,...

Vulnerable code:
function hashToG1(bytes memory message) internal view returns (G1Point memory result) {
    bytes memory uniform_bytes = expandMsg("BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_", message, 0x80);

bytes32 private constant DST_PART1 = "BLS_SIG_BLS12381G1_XMD:SHA-256_S";
bytes11 private constant DST_PART2 = "SWU_RO_NUL_";
uint8 private constant DST_LEN = 43;

function hashToG1(bytes memory message) internal view returns (G1Point memory result) {
    bytes memory uniform_bytes = expandMsgBLS(message, 0x80);
    // ...
}

function expandMsgBLS(bytes memory message, uint8 n_bytes) internal pure returns (bytes memory) {
    bytes memory zpad = new bytes(0x40);
    bytes memory b_0 = abi.encodePacked(zpad, message, uint8(0x00), n_bytes, uint8(0x00), DST_PART1, DST_PART2, DST_LEN);
    // ... rest of expandMsg logic using DST_PART1, DST_PART2, DST_LEN
}
HIGH severity: Users can cast vote without actually holding tokens. **Description:** In `voting.rs` users can vote for protocol decisions with the `drvs` tokens they posses in their respective client accounts. However this mechanism can be abused by users where users can vote with inflated token balances by exploiting the timing of deposits and withdrawals during...

Vulnerable code:
// In `client_community.rs` `update()` during deposit
if available_tokens != self.header.drvs_tokens {
    // 50,000 != 0 - TRUE

    community_state.header.upgrade()?.drvs_tokens += 50,000;

    if self.header.slot <= community_state.header.voting_start_slot {
        // 0 <= 60 - TRUE
        self.header.current_voting_counter = 0;
        self.header.current_voting_tokens = 50,000;
    }
    self.header.slot = 50;
    self.header.drvs_tokens = 50,000;
}

// In voting.rs
if data.voting_counter != community_state.header.voting_counter {
    // 1 == 1 - Check passes
}

if client_community_state.header.slot <= community_state.header.voting_start_slot {
    // 50 <= 60 - TRUE
    voting_tokens = client_community_state.header.drvs_tokens; // 50,000
    client_community_state.header.current_voting_counter = 1;
    client_community_state.header.current_voting_tokens = 50,000;
} else {
    voting_tokens = client_community_state.header.current_voting_tokens;
}

if voting_tokens > 0 {
    if client_community_state.header.last_voting_counter >= client_community_state.header.current_voting_counter {
        // 0 >= 1 - FALSE, check passes
    }

    // Vote is cast successfully
    community_account_header.voting_incr += 50,000; // Example: user votes INCREMENT
    client_community_state.header.last_voting_counter = 1;
    client_community_state.header.last_voting_tokens = 50,000;
}

// In client_community.rs update() during deposit
if available_tokens != self.header.drvs_tokens {
    // 100,000 != 50,000 - TRUE

    community_state.header.upgrade()?.drvs_tokens += 50,000;

    if self.header.slot <= community_state.header.voting_start_slot {
        // 50 <= 100 - TRUE
        self.header.current_voting_counter = 2; // Updates to period 2
        self.header.current_voting_tokens = 100,000; //
    }
    self.header.slot = 120; // Updated to current slot after the check
    self.header.drvs_tokens = 100,000;
}
HIGH severity: Missing Version Validation for Private Clients Account in `new_private_client`. **Description:** The `new_private_client()` function fails to validate that the provided `private_clients_acc` account matches the protocol version specified in `root_state`.

Since different protocol versions use different PDA seeds (which include the version), each version has its own isolated...

Vulnerable code:
// src/program/processor/new_root_account.rs:291
let seed = get_seed_bytes(version, account_type::PRIVATE_CLIENTS);
// get_seed_bytes includes version in the seed (lines 73-77)
let bump_seed = check_new_program_account(
    private_clients_acc,
    &drvs_auth,
    program_id,
    &seed,
    AccountType::PrivateClients,
)?;

// src/program/processor/new_private_client.rs:70-100
let private_clients_acc: &AccountInfo<'_> = next_account_info!(account_iter)?;
// ...
let root_state: &RootState = RootState::from_account_info(root_acc, program_id)?;

// ❌ No validation of `private_clients_acc` here

if !root_state.is_private_mode() {
    bail!(DeriverseErrorKind::MustBeInPrivateMode);
}

// Directly uses private_clients_acc without version check
let private_clients = private_clients_acc
    .try_borrow_data()
    .map_err(|err| drv_err!(err.into()))?;
HIGH severity: Airdrop Implementation Issues: Unvalidated Ratio and Potential Transfer Direction Mismatch. **Description:** The `airdrop` function is intended to convert user-earned `points` into DRVS tokens. However, the implementation has the following issues:

**Issue 1: Unvalidated Ratio Parameter**

The `AirdopOnChain` trait implementation only validates data format, not the `ratio` value:

The...

Vulnerable code:
impl AirdopOnChain for AirdropData {
    fn new(instruction_data: &[u8]) -> Result<&Self, DeriverseError> {
        bytemuck::try_from_bytes::<Self>(instruction_data)
            .map_err(|_| drv_err!(InvalidClientDataFormat))  // Only format check
    }
}

amount = ((amount as f64) * data.ratio) as u64;  // No validation on ratio!

let transfer_to_taker_ix = spl_token_2022::instruction::transfer_checked(
    &spl_token_2022::id(),
    drvs_client_associated_token_acc.key,  // FROM: User's account
    drvs_mint.key,
    drvs_program_token_acc.key,            // TO: Program's account
    signer.key,                            // Authority: User
    &[signer.key],
    amount,
    decs_count as u8,
)?;

invoke(
    &transfer_to_taker_ix,
    &[
        token_program.clone(),
        drvs_client_associated_token_acc.clone(),  // User account (source)
        drvs_mint.clone(),
        drvs_program_token_acc.clone(),            // Program account (destination)
        signer.clone(),                            // User signature
    ],
)?;
HIGH severity: `update_records` always resets `fees_ratio` to 1 whenever a new currency is added. **Description:** The `update_records` function is used to update the `client_community_acc` by including more `ClientCommunityRecord` for every currency token. However, within this function the `fees_ratio` is always being reset to `1.0` whenever a new currency is added. This occurs because the...

Vulnerable code:
self.data = unsafe {
                Vec::from_raw_parts(
                    dividends_ptr as *mut ClientCommunityRecord,
                    self.header.count as usize,
                    self.header.count as usize,
                )
            };
            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;
            }
HIGH severity: Stale Funds Data in Leverage Validation Due to Missing Funding Rate Settlement. **Description:** In `perp_change_leverage`, the code calls `check_soc_loss` but omits `check_funding_rate` before validating leverage constraints. As a result, `check_client_leverage` uses stale funds data when computing evaluation and checking leverage limits. This can allow invalid leverage...

Vulnerable code:
engine.check_soc_loss(client_state.temp_client_id)?;
    if engine.check_short_margin_call()? < MAX_MARGIN_CALL_TRADES {
        engine.check_long_margin_call()?;
    }
    engine.check_rebalancing()?;
    engine.change_edge_px(client_state.temp_client_id);

    engine.check_client_leverage(client_state.temp_client_id)?;

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;
                if funding_funds != 0 {
                    info.add_funds(funding_funds).map_err(|err| drv_err!(err))?;
                    info5.funding_funds = info5
                        .funding_funds
                        .checked_add(funding_funds)
                        .ok_or(drv_err!(DeriverseErrorKind::ArithmeticOverflow))?;
                    self.state.header.perp_funding_funds -= funding_funds;
                    change = true;

engine.check_client_leverage(client_state.temp_client_id)?;

    pub fn check_client_leverage(&self, temp_client_id: ClientId) -> DeriverseResult {
        let info = unsafe { &*(self.client_infos.offset(*temp_client_id as isize)) };
        let info2 = unsafe { &*(self.client_infos2.offset(*temp_client_id as isize)) };
        let evaluation = self.calculate_evaluation(info)?;
        let perp_value = self.calculate_perp_value(info);

        self.check_client_leverage_from(info, info2, evaluation, perp_value)
    }
HIGH severity: Fee Discount Calculation Ignores Base Currency Value Differences. **Description:** The fee discount calculation in `fees_deposit` uses the raw token amount divided by decimal factor, without considering the actual value differences between base currencies. This can lead to unfair discount distribution when multiple base currencies with different values are...

Vulnerable code:
let dec_factor = get_dec_factor(community_state.base_crncy[crncy_index].decs_count) as f64;
let prepayment = data.amount as f64 / dec_factor;
let fees_discount = community_state.fees_discount(prepayment);

Supported Base Currency
Current Support:

USDC: Circle USD Stablecoin

Future Expansion:

- Additional base currencies may be added through governance

- Multi-denomination support under consideration

- Community can propose new base currencies
HIGH severity: Missing ps check causes Dividend Loss. **Description:** In the perp engine's fee distribution logic, both `match_ask_orders` and `match_bid_orders` functions allocate pool fees without checking if there is any liquidity provider in the spot pool. This causes a loss of dividends for DRVS token holders when there is no liquidity...

Vulnerable code:
let delta = self.state.header.protocol_fees - prev_fees;
let pool_fees = ((1.0 - self.spot_pool_ratio) * delta as f64) as i64;
self.state.header.protocol_fees -= pool_fees;
self.state.header.pool_fees = self
    .state
    .header
    .pool_fees
    .checked_add(pool_fees)
    .ok_or(drv_err!(DeriverseErrorKind::ArithmeticOverflow))?;

let delta = self.state.header.protocol_fees - prev_fees;
let pool_fees = if self.state.header.ps == 0 {
    0  // No pool allocation when there is no liquidity provider
} else {
    ((1.0 - self.spot_pool_ratio) * delta as f64) as i64
};
self.state.header.protocol_fees -= pool_fees;
self.state.header.pool_fees = self
    .state
    .header
    .pool_fees
    .checked_add(pool_fees)
    .ok_or(drv_err!(DeriverseErrorKind::ArithmeticOverflow))?;
HIGH severity: Incorrect Fee Scaling in `spot_lp`. **Description:** The fee calculation in the `spot_lp` instruction uses an incorrect decimal factor when converting `last_px` to a human-readable price when trying to determine how much money is needed to purchase the minimum quantity of an asset. The code divides `last_px` by...

Vulnerable code:
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);

let mut px = (self.state.header.crncy_tokens as f64 * self.df
            / self.state.header.asset_tokens as f64) as i64; // <= crncy_token_decs_count + 9 + asset_token_decs_count - crncy_token_decs_count - asset_token_decs_count = 9

px = 2 * 1e9
(instr_state.header.last_px as f64
        / get_dec_factor(instr_state.header.asset_token_decs_count) as f64) = 2*1e9 / 1e8 = 20.
HIGH severity: Makers' rebates are not paid in case of swap. **Description:** When orders are matched, we choose between AMM and orderbook based on which offers better prices. In case of `swap` when orderbook doesnt exist, we trade with only AMM, when it exists we choose better of both.. we create a temporary client's primary account.. while looking for...

Vulnerable code:
//inside swap.rs
    if buy {
        if price > px || engine.cross(price, OrderSide::Ask) {
            (q, traded_sum, trades, _) =
                engine.match_orders(price, qty, &mut client_state, None, OrderSide::Ask)?;
        }

let fill_static_args = FillStaticArgs {
            fee_rate,
            ref_discount,
            taker_client_id: client.id,
            side,
        };

if remaining_qty > 0 {
                        self.fill(
                            &mut line,
                            &mut remaining_qty,
                            &mut sum,
                            &mut trades,
                            &mut total_fees,
                            &mut total_rebates,
                            &mut fees_prepayment,    // 0
                            &fill_static_args, // contains fee rate as 0
                            //ref_discount,
                            //client.id,
                            //fee_rate,
                            //side,
                        )?;
                    }
                }
HIGH severity: `perp-change-leverage` uses stale `perp-underlying-px`. **Description:** The [perp_change_leverage](https://github.com/deriverse/protocol-v1/blob/30b06d2da69e956c000120cdc15907b5f33088d7/src/program/processor/perp_change_leverage.rs) function fails to call [engine.state.set_underlying_px(accounts_iter)?]() before performing various checks which involve.....

Vulnerable code:
//@audit  present in perp_withdraw, perp_order_cancel, perp_mass_cancel, etc.
    //engine.state.set_underlying_px(accounts_iter)?;
    engine.check_soc_loss(client_state.temp_client_id)?;
    if engine.check_short_margin_call()? < MAX_MARGIN_CALL_TRADES {
        engine.check_long_margin_call()?;
    }
    engine.check_rebalancing()?;
    engine.change_edge_px(client_state.temp_client_id);

    engine.check_client_leverage(client_state.temp_client_id)?;
HIGH severity: Shorts can withdraw full available funds instead of being restricted to margin call limits in `perp-withdraw`. **Description:** In [perp_withdraw.rs](https://github.com/deriverse/protocol-v1/blob/f611019674f668681c3dbe074f8b7bc2ba846c97/src/program/processor/perp_withdraw.rs#L99), the margin call detection logic fails to detect for shorts ,where...

Vulnerable code:
engine.check_long_margin_call()?;
        engine.check_short_margin_call()?;
        //@audit both are long, one should be short
        let margin_call = engine.is_long_margin_call() || engine.is_long_margin_call();
        if !margin_call {
            engine.check_rebalancing()?;
}

let amount = if margin_call {
    // Limited withdrawal - only excess margin above requirements
    let margin_call_funds = funds.min(engine.get_avail_funds(client_state.temp_client_id, true)?);
    if margin_call_funds <= 0 {
        bail!(ImpossibleToWithdrawFundsDuringMarginCall);
    }
    // Restricted amount
} else if data.amount == 0 {
    funds  // Full funds available - NO RESTRICTIONS
} else {
    // Normal withdrawal
};

let margin_call = engine.is_long_margin_call() || engine.is_short_margin_call();