As of 30 Oct 2026, Parity Finance Liquid Staking & Early Purchase has 2 audit reports by 1 security firm on Trustblock; the latest is by Quantstamp, dated 3 Jun 2025. Those reports list no open critical findings.
Record last updated . It holds audits and findings, contracts and incidents up to Oct 2026. Trustblock does not review code.
Newest first
| Audit | Security firm | Date | Scope | Critical findings | Report |
|---|---|---|---|---|---|
| Parity Finance Liquid Staking & Early Purchase | Security firm: Quantstamp | Date: | Scope: Files named in the report | No open critical findings | Open · certificate.quantstamp.com |
| Parity Finance | Security firm: Quantstamp | Date: | Scope: Repository at commit 9eee7b5 | No open critical findings | Open · certificate.quantstamp.com |
Statuses are the security firm’s own, from each report.
| Severity | Open | Acknowledged | Fixed |
|---|---|---|---|
| Critical | 0 | 0 | 0 |
| High | 0 | 0 | 6 |
| Medium | 2 | 0 | 8 |
| Low | 8 | 1 | 15 |
The `liquid_staking::restake_expired_withdrawal::handler()` function restakes the expired withdrawals by minting LST tokens back to the stakers. However, it does not transfer the relevant user's base tokens (funded by the depositor) from the `window_base_token_account` to the `pair_base_token_account`. Therefore, these base tokens will remain locked in the `window_base_token_account`, preventing the closing of the `withdrawal_window` in the `liquid_staking::close_withdrawal_window::handler()` function.
Open the reportDuring the token purchase phase in the `early-purchase` program, buyers transfer their payments (either SPL tokens to the `sale_payment_ata` or SOL directly to the Sale PDA). The sale organizer (acting through a Guardian) deposits the `sale.purchase_mint` tokens for distribution after the sale ends. However, the program lacks a mechanism to transfer the collected buyer payments from the `Sale` PDA and ATA accounts to the sale organizer or a designated treasury. It is also noted that the address of the user initiating the sale is not saved in the `Sale` PDA, and only guardians with `deposit_tokens` permission can deposit the `sale.purchase_mint` tokens.
Open the reportThe `early_purchase::deposit_tokens::handler()` function is intended to enable a Guardian to deposit a specified quantity of `purchase_mint` tokens (`params.amount_to_deposit`) into the sale token vault `sale_purchase_ata`. However, the `token::transfer` CPI within this instruction incorrectly uses `sale.calculate_base_purchase_cost(amount_to_deposit)` as the amount argument for the transfer instead of the `amount_to_deposit`, resulting in a significantly different value: ```rust token::transfer( CpiContext::new( purchase_program.to_account_info(), token::Transfer { from: authority_purchase_ata.to_account_info(), to: sale_purchase_ata.to_account_info(), authority: authority.to_account_info(), }, ), sale.calculate_base_purchase_cost(amount_to_deposit), ) ```
Open the reportThe `early_purchase::redeem_receipt::handler()` function is responsible for transferring the `purchase_mint` tokens from the sale's ATA (`config_purchase_ata`) to the buyer's ATA (`buyer_purchase_ata`) during sale receipt redemption. However, the `token::transfer` CPI is incorrectly configured with the `buyer` as the authority for this transfer: ```rust token::transfer( CpiContext::new( purchase_program.to_account_info(), token::Transfer { from: config_purchase_ata.to_account_info(), to: buyer_purchase_ata.to_account_info(), authority: buyer.to_account_info(), }, ), num_tokens_pending, ) ```
Open the reportThe Early Purchase program appears to be completly missing a test suite. Contracts that are not extensively tested are at high risk of serious bugs that can impact the protocol's proper functioning and security. While audits can help developers to find bugs, they are not a substitute for thorough unit and integration testing.
Open the reportSale accounts can be created by any user and they are assigned as their admins. Additionally, receipt PDAs are derived with the following seeds at the buyer's first token purchase: ``` seeds = [ Receipt::PREFIX.as_bytes(), &buyer.key.to_bytes() ] ``` This implies that `Receipt` accounts are initialized in a 1:1 relation to buyer accounts, allowing an attacker to create a receipt and purchase an amount of a worthless token and redeem the receipt against a sale that holds actual, legitimate tokens.
Open the reportThe `early_purchase::end_sale::handler()`function is used by a guardian to end a specific sale. However, the this function does not check that the referenced sale end time has passed (`sale.is_end_time_reached()`), allowing guardians to end any sale even before it has officially ended.
Open the reportThe instructions that initialize PDAs in the `early-purchase` program use the `space` parameter (`#[account(init, payer = ..., space = ...)]`) to specify the space needed for account creation. The `space` attribute expects the total space required for the account's data, including an 8-byte discriminator. However, the 8-byte discriminator is not accounted for in the size calculation. If insufficient space is allocated, it could lead to runtime errors during account initialization or data serialization issues later if data attempts to write past the allocated boundary.
Open the reportWhen users purchase tokens in the `early-purchase` program using SOL, the current SOL price is determined dynamically at the time of transaction execution using a Pyth Network oracle price feed. However, the current token purchase implementation does not allow users to specify a maximum amount of SOL they are willing to pay for their desired quantity of purchase tokens, only the number of tokens to buy. While the `sale.max_price_feed_age` check ensures the oracle price is not excessively stale, it does not protect against legitimate, recent price volatility that occurs before transaction confirmation. The `sale.purchase_mint` token price (`sale.payment_amount`) may also change without the buyer's knowledge.
Open the reportThe `early_purchase::update_sale::handler()` function permits `Guardians` with `update_sale` permissions to modify critical parameters such as `sale.purchase_mint`, `sale.payment_mint`, `sale.guard_purchases` and so on, even if the `sale.is_start_time_reached()`is `true`. Buyers typically make purchase decisions based on the observed state of the sale. If these parameters can be altered while the sale is active or just moments before a buyer's transaction is processed, a buyer might inadvertently pay a different price (potentially higher) or face different purchasing conditions than they initially expected. The `purchase_tokens` instruction calculates the cost dynamically at the time of execution based on the current `Sale` state. This could lead to user frustration, loss of trust, and scenarios where buyers feel deceived if, for instance, the token price effectively increases due to a mid-sale update.
Open the report`Sale` accounts are supposedly freezable by guardians/admins as they have an `is_frozen()` function that checks if `self.state == SaleState::Frozen`, which is called in the `PurchaseTokens` instruction handler. However there is no mechanism allowing authority to freeze and unfreeze sales as no instruction implements such a logic.
Open the reportA malicious user may transfer a few base tokens directly to a `window_base_token_account` of the `liquid-staking` program, preventing the `window_authority` to closing the ATA because of the following strict check: ```rust require!( window_base_token_account.amount == 0, StakingError::WindowHasActiveRequests ); ```
Open the reportThe `early_purchase::initialize_sale::handler()` function allows any user to create a new `Sale`. However, it does not implement any fee or significant disincentive beyond standard Solana transaction costs and account rent. This open and low-cost sale creation mechanism that can be exploited by malicious actors to spam the platform by creating a large number of sales or create sales with malicious properties.
Open the reportThe `parity-issuance` and `parity-staking` programs effectively track certain token-related changes locally in state variables such as `total_collateral` and `base_balance`. While there is nothing inherently wrong with tracking these values for program logic, they should not be relied upon as the authoritative source of truth for the actual token balances in vault accounts. Discrepancies may occur because users can transfer tokens directly to vaults, resulting in a mismatch between the tracked state and the actual vault balance. For example: 1. **`deposit_funds.rs` and `withdraw_funds.rs`**: The `total_collateral` field is updated locally in the `token_manager` account, but this value may diverge from the actual vault balance if tokens are transferred directly to the vault. 2. **`unstake.rs`**: The `base_balance` field in the `pool_manager` account is updated via a local variable, but it may not accurately reflect the true balance of the vault. 3. **`initialize_withdraw_funds.rs`**: Withdrawal validations are performed using the stored `total_collateral` value instead of the actual balance in the token account. While local state tracking is beneficial for efficiency and logic within the program, it is important to recognize that these values do not always reflect the real-time balance of vaults and should not be considered definitive for account balances.
Open the reportIn `liquid_staking::create_pair::handler()` function and `liquid_staking::update_pair_limits::handler()` are missing the validations that ensure `minimum_deposit < deposit_cap`.
Open the reportNo checks exist to enforce that the signer initializing the config is a specific pre-determined address. This allows anyone to initialize the config and takeover its control.
Open the reportThe `early_purchase::initialize_guardian::handler()` function allows an admin to create a new Guardian account, but provides no mechanism to update the `authority` Pubkey once set. This creates a significant risk if guardian credentials are compromised or need to be rotated. This is because the `update_guardian` instruction only enables updating the `permissions` but not the `authority`. Additionally, the `GuardianPermissions::manage_guardians` permission exists in the codebase but remains unused, suggesting an incomplete implementation of guardian management functionality.
Open the reportThe `liquid_staking::create_pair::handler()` function does not validate the `interval_apr_rate` value, and the current check in the `liquid_staking::update_pair_yield::handler()` only prevents setting the `interval_apr_rate` value to zero. The improper validations allow setting non-positive yield rate values because the rate is adjusted with `PRECISION` (`1e12`) in the exchange rate calculations. The `liquid_staking::create_pair::handler()` function does not limit the maximum value of `interval_apr_rate`, the `MAX_INTERVAL_APR_RATE` is only enforced in the `instructions::update_pair_yield::handler()` function.
Open the reportThe `liquid_staking::swap_lst::handler()` function enables holders of one LST to swap it for a different LST. However the function call on the `destination_pair` that checks if the deposit is too large incorrectly passes zero for the `base_token_amount` field of the function `destination_pair.check_excessive_deposit(quote_amount, base_token_amount)`. Moreover, it lacks a check to validate the the deposit meets the minimum bar via `check_minimum_deposit()`.
Open the reportThe `early_purchase::initialize_sale::handler()` function accepts a `max_price_feed_age` parameter that determines the maximum allowable staleness of price feed data. However, once set, this critical parameter cannot be modified. If an administrator configures this value incorrectly during initialization, there is no mechanism available to correct it, potentially leading to permanent operational issues with price feed validation.
Open the reportThe `liquid_staking::update_pair_limits::handler()`function creates a pair and takes in a `CreatePairParams` struct instance that holds the `stake_fee_bps`, `swap_fee_bps`, and `withdraw_fee_bps` values. The fee percentage BPS values can be set up to 10000, allowing the program to be configured with a 100% fee percentage.
Open the reportIn `liquid_staking::manage_guardian::handler()`function, an `unseal_authority` can add or remove guardians from the `AccessControl` Account. While the `AccessControl` account supports adding up to five guardians for security redundancy, the implementation lacks duplicate checking for each guardian `Pubkey`. This oversight allows the same guardian `Pubkey` to be added multiple times, potentially compromising the intended redundancy if multiple or all five guardian slots contain identical keys.
Open the reportThe `liquid_staking::create_token_metadata::handler()` function does not call `access_control.verify_unsealed()?;`, preventing the guardians from sealing the function in case of emergency.
Open the reportWhen a new slot is encountered, the `current_slot_volume` is updated twice: once in the `check_block_limit` function and again at the end of the handler function. This could lead to incorrect accounting of the current slot volume.
Open the reportSeveral token management functions in the parity-issuance program exhibit inconsistencies between the implemented admin checks and the documented behavior. Specifically, these functions are using `token_manager.owner` for admin verification, while the documentation states that `token_manager.admin` should be responsible for these operations. This discrepancy affects the following functions: 1. Adding gatekeepers 2. Removing gatekeepers 3. Updating token manager admin
Open the reportSeveral functions across the parity-contracts codebase lack comprehensive input validation for critical parameters. This includes missing checks for non-zero values, absence of bounds validation for numerical inputs such as fees, and lack of verification for account authorities and states. The affected areas span various operations such as staking, unstaking, minting, configuration updates, and token management. Several explicit examples: 1. `parity_staking` doesn't check if the quantity is 0 in the following functions 1. `stake()` 2. `unstake()` 2. `pt_staking` doesn't check if the quantity is 0 in the following functions 1. `pt_stake()` 2. `pt_unstake()` 3. `parity_issuance` doesn't check if the quantity is 0 in the following functions: 1. `deposit_funds()` 2. `withdraw_funds()` 3. `mint_admin()` 4. `mint()` 5. `redeem()`
Open the reportThe `mint` account in the `WithdrawFunds` struct lacks proper validation of its address. This allows an admin to pass in any mint account with an inflated supply, potentially bypassing withdrawal limits.
Open the reportIf a withdrawal is initiated twice before the first one is processed, the second one would overwrite the first one's pending withdrawal amount.
Open the reportThe `new_deposit_cap` can be set to a value less than the existing deposit amount in the vault account, potentially leading to inconsistencies.
Open the reportThe reports name a repository or files. Open a report to see its scope.
Is this your project? Show this record on your own website. The label states what the record holds, links to this page and updates by itself. It is free and needs no sign-in.
Automatic: the three strongest facts of this project’s own record (its last audit and two more), highlighted. They update with the record. Picking a fact turns Automatic off.
<head><script src="https://widget.trustblock.run/project-widget.min.js" defer></script></head><body><divdata-tb-project-widgetdata-tb-project-slug="parity-finance-liquid-staking-early-purchase"></div></body>
Paste the code where the label should appear. It uses no API credit and updates by itself.
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