Solana LP Locker - UNCX Network


Prepared by:

Halborn Logo

HALBORN

Last Updated 05/23/2025

Date of Engagement: December 9th, 2024 - December 19th, 2024

Summary

100% of all REPORTED Findings have been addressed

All findings

4

Critical

0

High

0

Medium

0

Low

0

Informational

4


1. Introduction

UNCX engaged Halborn to conduct a security assessment on a set of changes int their LP Locker Solana program beginning on March 11th, 2024, and ending on March 20th, 2024. The security assessment was scoped to the Solana Program provided in solana-lp-locker-monorepo GitHub repository. Commit hashes and further details can be found in the Scope section of this report.


This set of changes is based on a new extension of the calculation of the intrinsic growth of an LP position due to commissions to allow a user who has locked LP to claim this commission component while keeping the initial main LP locked.

2. Assessment Summary

Halborn was provided 8 days for the engagement and assigned one full-time security engineer to review the security of the Solana Programs in scope. The engineer is a blockchain and smart contract security expert with advanced smart contract hacking skills, and deep knowledge of multiple blockchain protocols.

The purpose of the assessment is to:

    • Identify potential security issues within the Solana Programs.

    • Ensure that smart contract functionality operates as intended.

 

In summary, Halborn did not identify any significant security risks. However, some improvements were highlighted as best practices, which were acknowledged by the UNCX team:

    • Add a check to validate the authority of user_token_coin and user_token_pc token accounts.

    • Add validation to reject default public keys in change_owner and set_dev and remove the redundant check in in confirm_pending_admin

    • Remove redundant fields in LpFeeCalc and TokenLock if they are not expected to be consumed in the program.

    • Add a check before the fee calculation process to ensure that current_locked_amount is greater than zero and create a specific error case to explicitly handle the scenario where it is not.


3. Test Approach and Methodology


Halborn performed a combination of a manual review of the source code and automated security testing to balance efficiency, timeliness, practicality, and accuracy in regard to the scope of the program assessment. While manual testing is recommended to uncover flaws in business logic, processes, and implementation; automated testing techniques help enhance coverage of programs and can quickly identify items that do not follow security best practices.

The following phases and associated tools were used throughout the term of the assessment:

    • Research into the architecture, purpose, and use of the platform.

    • Manual program source code review to identify business logic issues.

    • Mapping out possible attack vectors

    • Thorough assessment of safety and usage of critical Rust variables and functions in scope that could lead to arithmetic vulnerabilities.

    • Scanning dependencies for known vulnerabilities (`cargo audit`).

    • Local runtime testing (`cargo test-bpf` )



4. RISK METHODOLOGY

Every vulnerability and issue observed by Halborn is ranked based on two sets of Metrics and a Severity Coefficient. This system is inspired by the industry standard Common Vulnerability Scoring System.
The two Metric sets are: Exploitability and Impact. Exploitability captures the ease and technical means by which vulnerabilities can be exploited and Impact describes the consequences of a successful exploit.
The Severity Coefficients is designed to further refine the accuracy of the ranking with two factors: Reversibility and Scope. These capture the impact of the vulnerability on the environment as well as the number of users and smart contracts affected.
The final score is a value between 0-10 rounded up to 1 decimal place and 10 corresponding to the highest security risk. This provides an objective and accurate rating of the severity of security vulnerabilities in smart contracts.
The system is designed to assist in identifying and prioritizing vulnerabilities based on their level of risk to address the most critical issues in a timely manner.

4.1 EXPLOITABILITY

Attack Origin (AO):
Captures whether the attack requires compromising a specific account.
Attack Cost (AC):
Captures the cost of exploiting the vulnerability incurred by the attacker relative to sending a single transaction on the relevant blockchain. Includes but is not limited to financial and computational cost.
Attack Complexity (AX):
Describes the conditions beyond the attacker’s control that must exist in order to exploit the vulnerability. Includes but is not limited to macro situation, available third-party liquidity and regulatory challenges.
Metrics:
EXPLOITABILITY METRIC (mem_e)METRIC VALUENUMERICAL VALUE
Attack Origin (AO)Arbitrary (AO:A)
Specific (AO:S)
1
0.2
Attack Cost (AC)Low (AC:L)
Medium (AC:M)
High (AC:H)
1
0.67
0.33
Attack Complexity (AX)Low (AX:L)
Medium (AX:M)
High (AX:H)
1
0.67
0.33
Exploitability EE is calculated using the following formula:

E=meE = \prod m_e

4.2 IMPACT

Confidentiality (C):
Measures the impact to the confidentiality of the information resources managed by the contract due to a successfully exploited vulnerability. Confidentiality refers to limiting access to authorized users only.
Integrity (I):
Measures the impact to integrity of a successfully exploited vulnerability. Integrity refers to the trustworthiness and veracity of data stored and/or processed on-chain. Integrity impact directly affecting Deposit or Yield records is excluded.
Availability (A):
Measures the impact to the availability of the impacted component resulting from a successfully exploited vulnerability. This metric refers to smart contract features and functionality, not state. Availability impact directly affecting Deposit or Yield is excluded.
Deposit (D):
Measures the impact to the deposits made to the contract by either users or owners.
Yield (Y):
Measures the impact to the yield generated by the contract for either users or owners.
Metrics:
IMPACT METRIC (mIm_I)METRIC VALUENUMERICAL VALUE
Confidentiality (C)None (I:N)
Low (I:L)
Medium (I:M)
High (I:H)
Critical (I:C)
0
0.25
0.5
0.75
1
Integrity (I)None (I:N)
Low (I:L)
Medium (I:M)
High (I:H)
Critical (I:C)
0
0.25
0.5
0.75
1
Availability (A)None (A:N)
Low (A:L)
Medium (A:M)
High (A:H)
Critical (A:C)
0
0.25
0.5
0.75
1
Deposit (D)None (D:N)
Low (D:L)
Medium (D:M)
High (D:H)
Critical (D:C)
0
0.25
0.5
0.75
1
Yield (Y)None (Y:N)
Low (Y:L)
Medium (Y:M)
High (Y:H)
Critical (Y:C)
0
0.25
0.5
0.75
1
Impact II is calculated using the following formula:

I=max(mI)+mImax(mI)4I = max(m_I) + \frac{\sum{m_I} - max(m_I)}{4}

4.3 SEVERITY COEFFICIENT

Reversibility (R):
Describes the share of the exploited vulnerability effects that can be reversed. For upgradeable contracts, assume the contract private key is available.
Scope (S):
Captures whether a vulnerability in one vulnerable contract impacts resources in other contracts.
Metrics:
SEVERITY COEFFICIENT (CC)COEFFICIENT VALUENUMERICAL VALUE
Reversibility (rr)None (R:N)
Partial (R:P)
Full (R:F)
1
0.5
0.25
Scope (ss)Changed (S:C)
Unchanged (S:U)
1.25
1
Severity Coefficient CC is obtained by the following product:

C=rsC = rs

The Vulnerability Severity Score SS is obtained by:

S=min(10,EIC10)S = min(10, EIC * 10)

The score is rounded up to 1 decimal places.
SeverityScore Value Range
Critical9 - 10
High7 - 8.9
Medium4.5 - 6.9
Low2 - 4.4
Informational0 - 1.9

5. SCOPE

Files and Repository
(b) Assessed Commit ID: 8cdbd52
(c) Items in scope:
  • programs/uncx-solana-lp-locker/src/fee/mod.rs
  • programs/uncx-solana-lp-locker/src/fee/model.rs
  • programs/uncx-solana-lp-locker/src/instructions/lock_fee_claim.rs
↓ Expand ↓
Out-of-Scope: unchanged and already audited functionality of the following files:, programs/uncx-solana-lp-locker/src/accounts_ix/add_migrator_ix.rs, programs/uncx-solana-lp-locker/src/accounts_ix/add_whitelist_ix.rs, programs/uncx-solana-lp-locker/src/accounts_ix/admin_functions/admin_ix.rs, programs/uncx-solana-lp-locker/src/accounts_ix/increment_lock_lp.rs, programs/uncx-solana-lp-locker/src/accounts_ix/initialize_ix.rs, programs/uncx-solana-lp-locker/src/accounts_ix/lock_lp.rs, programs/uncx-solana-lp-locker/src/accounts_ix/migrate.rs, programs/uncx-solana-lp-locker/src/accounts_ix/mod.rs, programs/uncx-solana-lp-locker/src/accounts_ix/relock.rs, programs/uncx-solana-lp-locker/src/accounts_ix/remove_migrator_ix.rs, programs/uncx-solana-lp-locker/src/accounts_ix/remove_whitelist_ix.rs, programs/uncx-solana-lp-locker/src/accounts_ix/split_lock.rs, programs/uncx-solana-lp-locker/src/accounts_ix/withdraw_lp.rs, programs/uncx-solana-lp-locker/src/instructions/relock.rs, programs/uncx-solana-lp-locker/src/lib.rs, programs/uncx-solana-lp-locker/src/error.rs, programs/uncx-solana-lp-locker/src/instructions/initialize.rs, programs/uncx-solana-lp-locker/src/instructions/migrate.rs, programs/uncx-solana-lp-locker/src/instructions/split_lock.rs, programs/uncx-solana-lp-locker/src/instructions/withdraw_lp.rs, programs/uncx-solana-lp-locker/src/instructions/lock_lp.rs, programs/uncx-solana-lp-locker/src/instructions/relock.rs, programs/uncx-solana-lp-locker/src/instructions/split_lock.rs, programs/uncx-solana-lp-locker/src/raydium_port/state.rs, programs/uncx-solana-lp-locker/src/raydium_port/calc_reserves.rs, programs/uncx-solana-lp-locker/src/raydium_port/mod.rs, programs/uncx-solana-lp-locker/src/state/events.rs, programs/uncx-solana-lp-locker/src/state/configuration.rs, programs/uncx-solana-lp-locker/src/state/mod.rs, programs/uncx-solana-lp-locker/src/state/token_lock.rs, programs/uncx-solana-lp-locker/src/utils.rs, third party dependencies and economic attacks.
Out-of-Scope: New features/implementations after the remediation commit IDs.

6. Assessment Summary & Findings Overview

Critical

0

High

0

Medium

0

Low

0

Informational

4

Security analysisRisk levelRemediation Date
Pc and Coin token accounts authority check missingInformationalAcknowledged - 05/23/2025
Redundant checks and missing completed validation in role transition instructionsInformationalAcknowledged - 05/23/2025
Redundant fields in LpFeeCalc and TokenLockInformationalNot Applicable
Missing validation for current_locked_amount before fee calculationInformationalNot Applicable

7. Findings & Tech Details

7.1 Pc and Coin token accounts authority check missing

//

Informational

Description

The LPFeeClaim instruction requires several accounts to be provided, including user_token_coin and user_token_pc. These accounts are used to transfer the respective amount of coin and pc tokens from Raydium, corresponding to the calculated LP fees, which are transformed during the CPI withdraw call to Raydium.


While Raydium ensures that the mints associated with these accounts match the coin and pc token pair, it does not validate the authority of these accounts. Similarly, the LPFeeClaim instruction itself does not perform any authority validation for these accounts.


Although this does not pose a direct security risk, as these accounts are provided by the locker owner, it is recommended to validate their authority if the program expects them to be under the control of the locker owner. This would improve the robustness and integrity of the program.


lock_fee_claim.rs:

    /// CHECK: Safe. user token coin Account. user Account to credit.
    #[account(mut)]
    pub user_token_coin: UncheckedAccount<'info>,
    /// CHECK: Safe. user token pc Account. user Account to credit.
    #[account(mut)]
    pub user_token_pc: UncheckedAccount<'info>,

BVSS
Recommendation

Consider adding a check to validate the authority of both token accounts.

Remediation Comment

ACKNOWLEDGED: The UNCX team acknowledged this finding, since they prefer to offer flexibility to the locker owner in providing such accounts.

7.2 Redundant checks and missing completed validation in role transition instructions

//

Informational

Description

The change_owner and confirm_owner instructions implement a two-step ownership transfer mechanism. In the first step, change_owner accepts the new_admin public key as a parameter and sets it as the pending owner, provided it differs from the current admin. However, this instruction does not verify whether the new admin is already set as the current pending owner, nor does it prevent invalid values such as Pubkey::default.


change_owner.rs

pub fn handle_change_owner(ctx: Context<AdminIx>, new_admin_key: Pubkey) -> Result<()> {
    require_keys_neq!(ctx.accounts.config_account.config.admin_key, new_admin_key);

    ctx.accounts.config_account.set_new_admin_pending(new_admin_key);

The confirm_owner instruction (implemented as confirm_pending_admin) promotes the pending admin to active admin and resets the pending field. While the function includes a check to ensure that config.pending is Some, this is redundant because the instruction context (AdminIxConfirmOwnership) already enforces:

  • That new_admin is a signer and matches the current config.pending.

  • That admin_sign is a signer and matches config.admin_key.

Given these constraints, the presence of a None value in config.pending is structurally impossible at runtime, making the internal check unnecessary and dead code in practice.

admin_ix.rs

pub struct AdminIxConfirmOwnership<'info> {
    /// Signer of the Pending Admin Key
    #[account(constraint =&new_admin.key()==config_account.config.pending.as_ref().unwrap()@ UncxLpError::InvalidAccountError)]
    new_admin: Signer<'info>,
    //current admin
    #[account(constraint =admin_sign.key()==config_account.config.admin_key)]
    admin_sign: Signer<'info>,
    ///Configuration Account PDA
    #[account(mut,address=uncx_config_pda_key::ID)]
    pub config_account: Account<'info, ConfigurationAccount>,
}

confirm_pending_admin.rs

pub fn handle_confirming_pending_owner(ctx: Context<AdminIxConfirmOwnership>) -> Result<()> {

    ctx.accounts.config_account.confirm_pending_admin()?;

configuration.rs

pub fn confirm_pending_admin(&mut self)-> anchor_lang::prelude::Result<()> {
        let Some(pending_admin) = self.config.pending else {
            return err!(UncxLpError::NoPendingAdmin);
        };
        self.config.admin_key = pending_admin;
        //reset pending admin
        self.config.pending = None;

    Ok(())

A similar pattern occurs in the set_dev instruction. The current admin must provide the new developer's public key, and while the program checks that this key differs from the current developer, it does not validate against default values like Pubkey::default.


set_dev.rs

pub fn handle_set_dev(ctx: Context<AdminIx>, new_dev_addr: Pubkey) -> Result<()> {
    require_keys_neq!(ctx.accounts.config_account.config.dev_addr, new_dev_addr);

    ctx.accounts
        .config_account
        .set_developer_address(new_dev_addr);

Although it is ultimately the administrator's responsibility for supplying valid input and can recover from misconfigurations (e.g., via remove_pending_admin or another change_owner call), incorporating stricter validation and eliminating unreachable logic would improve code clarity, reduce potential confusion during audits, and align with robust development standards.

BVSS
Recommendation

To improve the robustness, clarity, and maintainability of the role transition logic, the following adjustments are recommended:

  • In change_owner, add a check to prevent reassigning the same address that is already pending, which would otherwise lead to redundant state transitions and reject default or invalid public keys (Pubkey::default()) to prevent accidental misconfiguration.

  • In set_dev, similarly validate that the new developer key is not a default or zeroed address.

  • Remove the Some check on config.pending and the corresponding error return in the confirm_pending_admin instruction if it is None. Since the instruction context enforces that the pending admin must sign the transaction, together with the current admin, and match the value of config.pending, it is guaranteed that config.pending is Some.


Remediation Comment

ACKNOWLEDGED: The UNCX team acknowledged this finding.

7.3 Redundant fields in LpFeeCalc and TokenLock

//

Informational

Description

The LpFeeCalc struct encompasses several parameters for calculating accumulated LP token fees, including user_share. Based on its nomenclature and intended functionality, user_share ostensibly represents the user's proportionate stake in the locked liquidity. However, this parameter is redundant since the TokenLock struct already maintains the current_locked_amount, which inherently reflects the user's participation in the liquidity pool. Additionally, the user_share field in LpFeeCalc appears to be directly derived from current_locked_amount, resulting in unnecessary data duplication.


model.rs:

pub(crate) struct LpFeeCalc {
    pub(crate) last_k: U128,
    pub(crate) current_k: U128,
    pub(crate) current_total_supply: u64,
    pub(crate) last_total_supply: u64,
    pub(crate) user_share : u64,
}

Furthermore, the TokenLock struct includes coin_accrued_fee and pc_accrued_fee, which, according to their names, are intended to represent the fees claimed in both tokens. Nevertheless, these fields are neither utilized nor updated within the program, rendering them superfluous

token_lock.rs:

pub struct TokenLock {
    pub bump: u8,
    /// The raydium amm pool id/amm pair lp mint address
    // keeping lp-token first for easier indexing via gpa.
    pub amm_id: Pubkey,
    pub lp_mint: Pubkey,
    /// incremental global locker_count value.
    //stored earlier for easier indexing
    pub lock_global_id: u64,
    ///the data the token was locked at ,stored in unix timestamp
    pub lock_date: i64,
    ///the data the token is unlocked at ,stored in unix timestamp
    pub unlock_date: i64,
    /// the country code of the locker/business
    pub country_code: u8,
    //amount of lp locked at the time of creating the lock
    pub initial_lock_amount: u64,
    //amount of lp locked at any specific time after the initial lock
    pub current_locked_amount: u64,
    /// who can withdraw the lp from the lock after specified duration is complete aka owner
    pub lock_owner: Pubkey,
    /// last stored constant product
    pub last_coin_reserve : u64,
    pub last_pc_reserve : u64,
    /// last sync lp supply
    pub last_recorded_lp_supply: u64,
    /// fee claimed so far in terms of lp tokens
    pub fee_on_locked_lp_claimed: u64,
    /// fee claimed in token 0
    pub coin_accrued_fee: u64,
    pub pc_accrued_fee: u64,

The presence of these unused fields may lead to increased transaction costs due to unnecessary data storage and serialization during program operations.

BVSS
Recommendation

Consider removing those fields in TokenLock if they are not expected to be consumed in the program.

Remediation Comment

NOT APPLICABLE: The UNCX team stated that the issue is not applicable.

7.4 Missing validation for current_locked_amount before fee calculation

//

Informational

Description

In the handle_lp_fee_claim instruction handler, the fee calculation logic depends on current_locked_amount (the amount of currently locked LP tokens) being greater than zero.


However, the code does not perform an explicit check to ensure that current_locked_amount is indeed greater than zero before proceeding with the commission calculation. Although there is a condition that checks that the calculated commission is less than current_locked_amount and greater than zero, this does not replace a direct check that current_locked_amount is positive.


This lack of a specific error handler or a more explicit check makes debugging and understanding the failure more difficult. Users and developers may not immediately realize that the issue stems from a zero balance of locked liquidity, slowing down the resolution process and potentially causing confusion.


lock_fee_claim.rs:

pub fn handle_lp_fee_claim(
	ctx: Context<ClaimLockedLpFee>,
	lock_id: u64,
) -> Result<()> {
	let lp_locker_acc = ctx.accounts.lp_locker_acc.deref();
	let raydium_amm_info = RaydiumAmmV1Info::try_from(&*ctx.accounts)?;

	// Calculate max_lp_fee_claimable based on feature flag
	#[cfg(not(feature = "raydium-withdraw-check"))]
	let max_lp_fee_claimable = {
		let fee = LpFeeCalc::new(&raydium_amm_info, lp_locker_acc)?
			.calc_user_lp_fee(lp_locker_acc.current_locked_amount)
			.ok_or(UncxLpError::LpFeesCalcFailed)?;

BVSS
Recommendation

To address this issue, consider to:

  • Introduce a direct check at the beginning of the fee calculation process to ensure that current_locked_amount is greater than zero.

  • Create a specific error case to explicitly handle this scenario where current_locked_amount is zero. This will provide developers and users with clear and actionable feedback about the issue.

Remediation Comment

NOT APPLICABLE: The UNCX team stated that the issue is not applicable.

8. Automated Testing

Static Analysis Report

Description

Halborn used automated security scanners to assist with detection of well-known security issues and vulnerabilities. Among the tools used was cargo audit, a security scanner for vulnerabilities reported to the RustSec Advisory Database. All vulnerabilities published in https://crates.io are stored in a repository named The RustSec Advisory Database. cargo audit is a human-readable version of the advisory database which performs a scanning on Cargo.lock. Security Detections are only in scope. All vulnerabilities shown here were already disclosed in the above report. However, to better assist the developers maintaining this code, the auditors are including the output with the dependencies tree, and this is included in the cargo audit output to better know the dependencies affected by unmaintained and vulnerable crates.

Cargo Audit Results


ID

Crate

Desccription

RUSTSEC-2022-0093

ed25519-dalek

Double Public Key Signing Function Oracle Attack on ed255109-dalek

RUSTSEC-2024-0344

curve25519-dalek

Timing variability in curve25519-dalek's Scalar29::sub/`Scalar52::sub`

RUSTSEC-2025-0009

ring

Some AES functions may panic when overflow checking is enabled.

RUSTSEC-2025-0024

crossbeam-channel

crossbeam-channel: double free on Drop


Halborn strongly recommends conducting a follow-up assessment of the project either within six months or immediately following any material changes to the codebase, whichever comes first. This approach is crucial for maintaining the project’s integrity and addressing potential vulnerabilities introduced by code modifications.

© Halborn 2025. All rights reserved.