Neutral Trade

QuantstampFiles named in the report

Quantstamp reviewed Neutral Trade on 4 Jul 2025: 12 findings, 0 critical; 6 marked fixed, 6 acknowledged.

No open critical findings
Open the reportOpen Neutral Trade’s security record

Report hosted on certificate.quantstamp.com.

Trustblock does not review code.

Findings12

Statuses are the security firm’s own, from its report.

SeverityOpenAcknowledgedFixed
Critical000
High001
Medium001
Low064
HighRefund Calculation Uses Stale Share-Mint TotalFixed

The `refund_deposit()` function calculates `shares_to_revert` as `user_share_of_pending_deposits * last_total_shares_minted / PRECISION`, yet `bundle_temp_data.last_total_shares_minted` is **never reduced** after each refund. When multiple deposits are refunded in succession, earlier refunds shrink `cumulative_pending_deposits` but leave `last_total_shares_minted` unchanged; later iterations therefore cancel too many shares. The bundle ends with the correct underlying balance but an under‑reported `total_shares`, distorting share price and subsequent accounting. The same stale value is reused in `process_deposits.rs`, compounding the mismatch whenever refunds and new deposits occur in the same cycle.

MediumPrecision Loss Due to Division Before MultiplicationFixed

The current implementation for calculating management fees is susceptible to a loss of precision due to its reliance on sequential integer divisions. This will cause the protocol to undercharge users for management fees over the long term systematically. ```rust let time_fraction = seconds_elapsed .checked_mul(bundle.asset_precision as u128) .ok_or(ErrorCode::MathError)? .checked_div(SECONDS_IN_YEAR) .ok_or(ErrorCode::MathError)?; // calculate fee percentage: (time_fraction * annual_fee_rate) / 10000 let mgmt_fee_pct = time_fraction .checked_mul(annual_fee_rate) .ok_or(ErrorCode::MathError)? .checked_div(10000) // Convert from bps to decimal .ok_or(ErrorCode::MathError)?; ``` The calculation is performed in three steps: A `time_fraction` is calculated, representing the portion of a year that has passed since the last fee collection. This calculation involves a division that truncates any remainder. The `time_fraction` is then used to calculate a `mgmt_fee_pct`, which involves another division and potential truncation. Finally, this percentage is used to determine the `management_fee_shares`. Because integer division in computing discards the remainder, any fractional component is lost at each step. This error, while potentially small for any single calculation, compounds and guarantees that the final fee charged will be less than or equal to the correct amount, but never more. Over thousands of transactions and users, this results in a slow but steady leak of value from the protocol's treasury. This is also the case for the P&L for shares: ```rust let pnl_usd = pnl_per_share .checked_mul(user_bundle_account.shares) .ok_or(ErrorCode::MathError)? .checked_div(bundle.asset_precision as u128) .ok_or(ErrorCode::MathError)?; // Step 2: Second division let perf_fee_usd = pnl_usd .checked_mul(bundle.performance_fee as u128) .ok_or(ErrorCode::MathError)? .checked_div(10000) .ok_or(ErrorCode::MathError)?; // Step 3: Third division performance_fee_shares = perf_fee_usd .checked_mul(bundle.asset_precision as u128) .ok_or(ErrorCode::MathError)? .checked_div(share_price) .ok_or(ErrorCode::MathError)?; ```

LowCode ImprovementFixed

The code can benefit from the following validations and improvements: 1. Add `withdrawal_fee` to `ChangedCoreParams` and include `cooldown_period` plus `estimated_pending_withdrawal_value` in `WithdrawalRequested`. 2. `new_manager ` instruction is not needed in `change_manager()`, unless `new_manager` is required as additional signer. 3. Drop the unused `oracle_data` account from `set_max_deposit_amount()` or reference it explicitly. 4. Remove the redundant pause check in `end_distribution()`. 5. In `request_withdrawal ()`, since there is an earlier check that estimated pending withdrawls value is zero, it is possible to replace `checked_add()` for `user_bundle_account.estimated_pending_withdrawal_value` with `estimated_withdrawal_amount` with direct assignment. 6. Initialize `user_bundle_account.owner` in `initialize_bundle_depositor()` instead of `request_deposit()`. 7. Delete or annotate unused helpers like `fetch_account_balance()` in `bundle.rs`. 8. Reject `Pubkey::default()` in `change_bundle_master_admin()` to prevent null assignments. 9. Require the treasury to be a valid `TokenAccount` with `mint == asset_address` in all relevant instructions. 10. Declare `treasury_account` as `Account<'info, TokenAccount>` in `request_deposit()`. 11. The `receiver_address` in `add_strategy()` should be checked against the `Pubkey::default()`to prevent null assignments. 12. The `newkeeper` in `set_keeper()` should be checked against the `Pubkey::default()` to prevent null assignments.

LowEnabling a Strategy without Prior Removal Can Inflate Bundle AllocationFixed

The protocol assumes every strategy follows a strict life‑cycle: it is first added, later removed, and only then re‑enabled. The enable_strategy instruction, however, does not verify that `strategy_account.allowed` is currently false. If a bundle manager submits an enable_strategy call while the strategy is still marked as allowed—whether due to a failed or out‑of‑order `remove_strategy` transaction—the function blindly adds `strategy_account.allocation_bps` to `bundle_account.current_allocation_bps` a second time. This double‑counting breaks the bundle’s internal accounting, and may prevent subsequent strategy operations or open unintended allocation imbalances. Although unlikely in normal use, the absence of the guard leaves room for human error or automated scripts that batch transactions without checking intermediate results.

LowRefill Uses Stale Oracle Data if Feed Is Out‑of‑dateFixed

The `perform_refill()` function recomputes equity by adding the refill amount to `oracle_data.average_external_equity`. Yet it never verifies that the oracle price is fresh; if `last_update_time` is well in the past, the function records equity with outdated market data, skewing bundle accounting, share‑price calculations.

LowUpdating Allocation of a Disabled Strategy Breaks Bundle AccountingFixed

The `update_allocation()` function is designed to adjust the position size of an active strategy. Yet the instruction never checks that `strategy_account.allowed` is `true` before proceeding. If a bundle manager calls `update_allocation` on a strategy that has already been removed, the logic becomes harmful: 1. It subtracts the strategy’s previous `allocation_bps` from `bundle_account.current_allocation_bps`, even though that subtraction already occurred during `remove_strategy`. 2. It then adds the new allocation value and writes it to the strategy account. The modification in the bundle's allocation causes it to drift from the correct allocation, potentially turning negative or falling below the real sum of active strategies. Situations that can trigger the bug include: * Managers misunderstanding the life‑cycle and adjusting a removed strategy. * Transaction ordering issues where `remove_strategy` fails but a queued `update_allocation` succeeds.

Contracts in scope

No deployed address is recorded for this audit.

The report names a repository or files. Open the report to see its scope.

Repository and files the report names (1)

  • programs/ntbundle/**/*

Part of Neutral Trade’s security record

Neutral Trade
Audits2 audits by 2 firmsLast audit Jul 2025, by Quantstamp.
Critical findingsNo open critical findingsAcross 2 reports.
Open Neutral Trade’s security record

Security firm

Quantstamp

236 projects reviewed · 332 reports · latest Oct 2026

See Quantstamp’s audits

Something wrong on this page? A missing report, a wrong date, a finding that was fixed since. Tell us and we check it against its source.

Tell us