icon

Gorples Farm - Entangle Labs


Prepared by:

Halborn Logo

HALBORN

Last Updated 07/17/2024

Date of Engagement: June 4th, 2024 - June 12th, 2024

Summary

100% of all REPORTED Findings have been addressed

All findings

5

Critical

0

High

0

Medium

1

Low

2

Informational

2


1. Introduction

Entangle Labs has been a core contributor to Gorples (ex-Borpa). This security assessment report for Gorples examines the Farm program's resilience against potential Solana's specifics threats, vulnerabilities and attack vectors. This includes a thorough examination of the logic and correct functionality of all the provided instructions, and also their security maturity. The Farm program handles user-facing operations such as Deposit/Withdraw, Boost/Unboost and Lock/Redeem, and also admin-facing operations such as the Initializer, Create and Configure Pool instructions Set Admin and Set Rewards.

Entangle team engaged Halborn to conduct a security assessment of the Farm program, beginning on June 4th, 2024 and ending on June 12th, 2024. The security assessment was scoped to the Solana Programs provided in Entangle-Protocol/gorples-solana GitHub repository. Commit hashes and further details can be found in the Scope section of this report.

2. Assessment Summary

Halborn was provided 1 week for the engagement and assigned one full-time security engineer to review the security of the Solana Program 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 program functionality operates as intended.


In summary, Halborn identified some security concerns that were accepted by Entangle team. The main ones were the following:

    • Minimum and maximum slippage values should be enforced.

    • Arithmetic operations should have the checked wrapper for consistency and enhanced protection.

    • A two-step admin transfer mechanism should be enforced.


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 (solana-test-framework).

3.1 Out-of-scope

    • External libraries and financial-related attacks.

    • New features/implementations after/with the remediation commit IDs.

    • Changes that occur outside of the scope of PRs/Commits.

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
(a) Repository: gorples-solana
(b) Assessed Commit ID: 283cfbf
(c) Items in scope:
  • boost.rs
  • claim_redeem.rs
  • configure_farm.rs
↓ Expand ↓
Out-of-Scope:
Out-of-Scope: New features/implementations after the remediation commit IDs.

6. Assessment Summary & Findings Overview

Critical

0

High

0

Medium

1

Low

2

Informational

2

Security analysisRisk levelRemediation Date
Slippage values should be enforcedMediumRisk Accepted
Multiple unsafe arithmetic operationsLowRisk Accepted
Lack of Two-Step admin transfer mechanismLowRisk Accepted
Solana and Anchor versions are not currentInformationalAcknowledged
Multiple input validations are missingInformationalAcknowledged

7. Findings & Tech Details

7.1 Slippage values should be enforced

//

Medium

Description

The slippage term refers to the difference between the expected price of a trade and the price at which the trade is actually executed. To mitigate the risks associated with slippage, it is crucial to enforce a threshold for slippage values in Solana programs which interacts with automated market makers and/or decentralized exchanges in general.

In the Farm program, the handle_deposit function takes three arguments: amount, slippage0 and slippage1, as follows:

- src/instructions/handle_deposit.rs

pub fn handle_deposit(
    ctx: Context<Deposit>,
    amount: u64,
    slippage0: u64,
    slippage1: u64,
) -> Result<()> {

These parameters are then passed to a cross-program invocation (CPI) to the Chef program:

- src/instructions/handle_deposit.rs

    let to_deposit = add_liquidity(
        cpi,
        amount,
        ctx.accounts.harvest.pool.deposit_fee_rate,
        slippage0,
        slippage1,
    )?
    .get();

In the Chef program, the amount_with_slippage utility function is responsible for calculating the amount of tokens to be swapped, taking into account the slippage values. Subsequently, a Cross-Program Invocation (CPI) is initiated to execute the trade on the Raydium decentralized exchange.

If slippage values are not enforced, users are exposed to the risk of significant financial loss. Therefore, it is essential to ensure that the programs enforce appropriate threshold values to protect users and maintain the integrity of the system.

Proof of Concept

The following test case can be used to demonstrate that it is possible to perform deposits with both slippage0 and slippage1 values set to extreme amounts such as zero (0) and 999_999_999 revealing that there is no threshold enforced for slippage values.

  • PoC Code

  it("/// H-01 - Slippage values are not enforced", async () => {
  let lookupTableAccountChef = (
    await provider.connection.getAddressLookupTable(
      chef.lookupTableAddressExtra
    )).value;
    let poolAddr = POOLS[0];
    const userDeposit = await farmProgram
    .methods
    .deposit(
      new anchor.BN(100),
      new anchor.BN(0),
      new anchor.BN(999_999_999)
    )
    .accounts({
      < ... >
    })
    .signers([publicUserNeo])
    .instruction();
    let computeUnits = ComputeBudgetProgram.setComputeUnitLimit({
      units: 2_000_000,
    });
    const messageV0 = new web3.TransactionMessage({
      payerKey: publicUserNeo.publicKey,
      recentBlockhash: (await provider.connection.getLatestBlockhash("finalized"))
        .blockhash,
      instructions: [computeUnits, userDeposit],
    }).compileToV0Message([lookupTableAccountChef]);
    const transactionV0 = new web3.VersionedTransaction(messageV0);
    transactionV0.sign([publicUserNeo]);
    const txId = await provider.connection.sendTransaction(transactionV0, {
      skipPreflight: true,
    });
    console.log(`Deposited as user. Tx id: ${txId}`);
  });

  • Logs

slippage_poc
BVSS
Recommendation

It is recommended to enforce a threshold for the slippage0 and slippage1 values. This verification should be placed at the start of the scope of handle_deposit function in the Farm program.


Remediation Plan

RISK ACCEPTED: The Entangle team accepted the risk of this finding.

7.2 Multiple unsafe arithmetic operations

//

Low

Description

In the Farm program, the Cargo.toml file has overflow-checks = true enabled, which is a commendable practice. However, it is also recommended to incorporate safe arithmetic operations throughout the program code to further enhance security and reliability.

The provided code excerpts (non-exhaustive) reveal several occurrences of unsafe arithmetic operations.

- src/instructions/boost.rs

pub fn handle_boost(ctx: Context<Boost>, xborpa_amount: u64) -> Result<()> {
    ctx.accounts
        .harvest
        .handle_harvest(ctx.bumps.harvest.authority)?;
    require_gte!(
        ctx.accounts.harvest.virtual_balances.xborpa,
        xborpa_amount,
        CustomError::InsufficientFunds
    );
    // Substract old user position from pool
    let lp_amount = ctx.accounts.harvest.position.lp_token_amount;
    let current_multiplier = ctx.accounts.harvest.position.multiplier;
    let current_total_position = lp_amount + current_multiplier * lp_amount / 10000;
    ctx.accounts.harvest.pool.lp_supply_with_multiplier -= current_total_position;
    // Get new multiplier
    ctx.accounts.harvest.virtual_balances.xborpa -= xborpa_amount;
    ctx.accounts.harvest.pool.total_xborpa_allocated += xborpa_amount;
    ctx.accounts.harvest.position.xborpa_allocated += xborpa_amount;
    ctx.accounts.harvest.virtual_balances.total_xborpa_allocated += xborpa_amount;
    let new_multiplier = get_multiplier(
        MAX_BOOST,
        lp_amount,
        ctx.accounts.harvest.pool.lp_supply,
        ctx.accounts.harvest.position.xborpa_allocated as u128,
        ctx.accounts.harvest.pool.total_xborpa_allocated as u128,
    );
    ctx.accounts.harvest.position.multiplier = new_multiplier;
    // Update pool and user position
    let new_total_position = lp_amount + new_multiplier * lp_amount / 10000;
    ctx.accounts.harvest.pool.lp_supply_with_multiplier += new_total_position;
    Ok(())
}

In the claim_redeem instruction of the Farm program, the value of the variable to_mint is obtained by converting the value of an I80F48 type to a value of anu64 type.

Converting amount of type I80F48 to u64 can lead to precision loss, especially if amount has fractional components that will be truncated.

- src/instructions/claim_redeem.rs

pub fn handle_claim_redeem(ctx: Context<ClaimRedeem>, amount: u64) -> Result<()> {
    require_gte!(
        ctx.accounts.redeem.amount,
        amount,
        CustomError::InsufficientFunds
    );
    ctx.accounts.redeem.amount -= amount;
    let farm_config = ctx.accounts.farm_state.get_config();
    let mut amount = I80F48::from(amount - amount * farm_config.xborpa_redeem_fee / 10000);
    let cooldown = I80F48::from(farm_config.xborpa_cooldown);
    let redeem_started = ctx.accounts.redeem.started;
    let now = ctx.accounts.clock.unix_timestamp as u64;
    let elapsed = I80F48::from(
        now.checked_sub(redeem_started)
            .expect("time went backwards"),
    );
    let cooldown_ratio = I80F48::ONE - (elapsed / cooldown).clamp(I80F48::ZERO, I80F48::ONE);
    let haircut = cooldown_ratio * I80F48::from(farm_config.xborpa_haircut) / I80F48::from(10000);
    amount *= I80F48::ONE - haircut;
}

- src/instructions/harvest.rs

impl<'info> Harvest<'info> {
    pub fn handle_harvest(&mut self, authority_bump: u8) -> Result<()> {
        if !self.farm_state.is_started(self.clock.unix_timestamp as u64) {
            return Err(CustomError::NotYetStarted.into());
        }
        if self.position.last_claim_timestamp == 0 {
            self.position.last_claim_timestamp = self.clock.unix_timestamp as u64;
            return Ok(());
        }
        let raw_lp_position = self.position.lp_token_amount;
        let multiplier = self.position.multiplier;
        let total_position = raw_lp_position + raw_lp_position * multiplier / 10000;
        let last_claim = I80F48::from(self.position.last_claim_timestamp);
        self.position.last_claim_timestamp = self.clock.unix_timestamp as u64; // Update last claim
        let now = I80F48::from(self.clock.unix_timestamp);
        if self.pool.lp_supply_with_multiplier == 0 || total_position == 0 || now <= last_claim {
            return Ok(());
        }
        let elapsed_days = (now - last_claim) / I80F48::from(DAY);
        let elapsed_days_u64 = elapsed_days.to_u64().ok_or(CustomError::Overflow)?;
        let current_day = self
            .farm_state
            .get_current_day(self.clock.unix_timestamp as u64);
        /*solana_program::log::sol_log(&format!(
            "elapsed_days: {}, current_day: {}",
            elapsed_days_u64, current_day
        ));*/
        let mut user_rewards = 0;
        for day in 0..elapsed_days_u64 {
            // / 1_000_000_000_000 to adjust for formula taken from solidity with 18 decimals
            let emission_for_day = self
                .farm_state
                .get_emission_per_day((current_day - day) as u128)
                / 1_000_000_000_000;
            /*solana_program::log::sol_log(&format!(
                "emission_for_day: {} - {}",
                emission_for_day, day
            ));*/
            let pool_reward_for_day =
                emission_for_day * (self.pool.total_reward_share as u128) / 10000;
            let user_reward_for_day =
                pool_reward_for_day * total_position / self.pool.lp_supply_with_multiplier;
            user_rewards += user_reward_for_day;
        }
        if user_rewards == 0 {
            return Ok(());
        }
        let user_rewards: u64 = user_rewards.try_into().map_err(|_| CustomError::Overflow)?;
        let xborpa = user_rewards * self.pool.xborpa_percent / 10000;
        let borpa = user_rewards - xborpa;
        self.virtual_balances.xborpa += xborpa;
        self.farm_state.total_xborpa += xborpa;
BVSS
Recommendation

It is recommended to use checked wrappers like checked_add, checked_sub, checked_div and checked_mul when performing arithmetic operations.

To avoid rounding issues, it is recommended to round down to the first valid integer, considering token decimals, when converting from I80F48 types to u64 types.


Remediation Plan

RISK ACCEPTED: The Entangle team accepted the risk of this finding.

7.3 Lack of Two-Step admin transfer mechanism

//

Low

Description

The current implementation of the set_admin instruction utilizes a one-step procedure for authority delegation, which in this case is translated to setting the farm.admin in FarmState account to another address. This approach raises a security concern, as it lacks a safeguard against accidental or unintended delegations to unauthorized or malicious addresses.

- src/instructions/set_admin.rs

#[derive(Accounts)]
pub struct SetAdmin<'info> {
    /// Deployer address
    #[account(signer, address = DEPLOYER.parse().expect("Deployer key not set"))]
    pub deployer: Signer<'info>,

    /// Farm state
    #[account(mut, seeds = [FARM_ROOT, b"STATE"], bump)]
    pub farm_state: Box<Account<'info, FarmState>>,
}

pub fn handle_set_admin(ctx: Context<SetAdmin>, admin: Pubkey) -> Result<()> {
    ctx.accounts.farm_state.set_admin(admin);
    Ok(())
}

- src/state.rs

    pub fn set_admin(&mut self, admin: Pubkey) {
        self.admin = admin;
    }
BVSS
Recommendation

To mitigate this risk, it is recommended to enforce a multi-step admin transfer mechanism. In this approach, the current admin initiates the transfer by setting a specific address as pending_admin. Subsequently, the pending_admin accepts the transfer, completing the delegation process.


Remediation Plan

RISK ACCEPTED: The Entangle team accepted the risk of this finding.

7.4 Solana and Anchor versions are not current

//

Informational

Description

It was identifying during the assessment of the program Farm in-scope that its dependencies for the Anchor framework and also for Solana are not current.

[[package]]
name = "anchor-lang"
version = "0.29.0"

{ ... }

[[package]]
name = "solana-program"
version = "1.16.27"
BVSS
Recommendation

It is recommended to update dependencies to their current versions, as specified:

  • Solana: v1.18.15

  • Anchor: v0.30.0


Remediation Plan

ACKNOWLEDGED: The Entangle team acknowledged this finding.

7.5 Multiple input validations are missing

//

Informational

Description

The provided Farm program in-scope performs some administrative tasks such as configure_farm, configure_pool and add_pool. These instructions are closely related to state modifications that will affect the FarmState account.

In the Configure Pool instruction, there is no validation for the parameters total_reward_share, deposit_fee_rate and xborpa_percent, as follows:

- src/instructions/configure_pool.rs

pub fn handle_configure_pool(
    ctx: Context<ConfigurePool>,
    total_reward_share: u64,
    deposit_fee_rate: u64,
    xborpa_percent: u64,
) -> Result<()> {
    ctx.accounts
        .farm_state
        .register_pool_allocation(ctx.accounts.pool.total_reward_share, total_reward_share)?;
    ctx.accounts.pool.total_reward_share = total_reward_share;
    ctx.accounts.pool.deposit_fee_rate = deposit_fee_rate;
    ctx.accounts.pool.xborpa_percent = xborpa_percent;
    Ok(())
}

In the Configure Farm instruction, there is no validation in place for the xborpa_cooldown, xborpa_redeem_fee and xborpa_haircut parameters, as follows:

pub fn handle_configure_farm(
    ctx: Context<ConfigureFarm>,
    xborpa_cooldown: u64,
    xborpa_redeem_fee: u64,
    xborpa_haircut: u64,
) -> Result<()> {
    ctx.accounts
        .farm_state
        .set_params(xborpa_cooldown, xborpa_redeem_fee, xborpa_haircut);
    Ok(())
}

In the Create Pool instruction, there is no validation in place for the parameters total_reward_share, deposit_fee_rate and xborpa_percent, as follows:

- src/instructions/create_pool.rs

pub fn handle_create_pool(
    ctx: Context<CreatePool>,
    total_reward_share: u64,
    deposit_fee_rate: u64,
    xborpa_percent: u64,
) -> Result<()> {
    ctx.accounts.pool.mint0 = ctx.accounts.mint0.key();
    ctx.accounts.pool.mint1 = ctx.accounts.mint1.key();
    ctx.accounts
        .farm_state
        .register_pool_allocation(0, total_reward_share)?;
    ctx.accounts.pool.total_reward_share = total_reward_share;
    ctx.accounts.pool.deposit_fee_rate = deposit_fee_rate;
    ctx.accounts.pool.xborpa_percent = xborpa_percent;
    Ok(())
}

All the values casted in these examples are used widely in the protocol, in instructions like harvest, deposit, withdraw, claim_redeem, and others. The lack of validation of the inputs given to configuration instructions can lead to miscalculations when executing user-facing instructions.

BVSS
Recommendation

It is recommended to perform input validation over the parameters that are given to administrative instructions.


Remediation Plan

ACKNOWLEDGED: The Entangle team acknowledged this finding.

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

Description

RUSTSEC-2022-0093

ed25519-dalek@1.0.1

Double Public Key Signing Function Oracle Attack on ed25519-dalek

RUSTSEC-2023-0033

borsh@0.10.3

Parsing borsh messages with ZST which are not-copy/clone is unsound

RUSTSEC-2023-0033

borsh@0.9.3

Parsing borsh messages with ZST which are not-copy/clone is unsound

RUSTSEC-2021-0145

atty@0.2.14

Potential unaligned read

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.