Prepared by:
HALBORN
Last Updated 07/30/2024
Date of Engagement by: July 11th, 2024 - July 15th, 2024
100% of all REPORTED Findings have been addressed
All findings
4
Critical
0
High
0
Medium
0
Low
3
Informational
1
Entangle Labs
team engaged Halborn to conduct a security assessment on a series of changes in their Gorples
(ex Borpa) programs, SOL Chef Solana Program
and SOL Farm Solana Program
, beginning on July 7th, 2024 and ending on July 15th, 2024. The security assessment was scoped to the smart contracts provided in the GitHub repository gorples-solana, commit hashes, and further details can be found in the Scope section of this report.
The recent updates have involved modifications to the add_liquidity
, remove_liquidity
, swap
, and harvest
instructions with the following objectives:
Implementing continuous rewards distribution
Resolving operational issues related to Raydium integration on the mainnet
Halborn
was provided 3 days for the engagement and assigned two full-time security engineers to review the security of the Solana Programs in scope. The 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 smart contract functionality operates as intended.
In summary, Halborn
identified some security concerns, that were acknowledged or had its risk accepted by the Entangle Labs team
. The main ones were the following:
Balance Check Missing When Easy Mode in Add Liquidity
Insufficient verification of Refund Amounts in Add Liquidity
Users Rewards Loss if Farm Set Start Rewards After Deposits.
Note: As of the date of this report's finalization, no meaningful documentation of program performance and changes, as well as a functional test suite to perform the security tests locally, was provided. The extensive number of accounts and dependencies required to deploy the local environment for security testing, combined with the complexity of developing such an environment under these circumstances, exceeded the scope of this security assessment.
As a result, we were limited to conducting a comprehensive code review of the relevant changes. This limitation prevents us from guaranteeing 100% code security.
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).
EXPLOITABILIY METRIC () | METRIC VALUE | NUMERICAL 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 |
IMPACT METRIC () | METRIC VALUE | NUMERICAL 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 |
SEVERITY COEFFICIENT () | COEFFICIENT VALUE | NUMERICAL VALUE |
---|---|---|
Reversibility () | None (R:N) Partial (R:P) Full (R:F) | 1 0.5 0.25 |
Scope () | Changed (S:C) Unchanged (S:U) | 1.25 1 |
Severity | Score Value Range |
---|---|
Critical | 9 - 10 |
High | 7 - 8.9 |
Medium | 4.5 - 6.9 |
Low | 2 - 4.4 |
Informational | 0 - 1.9 |
Critical
0
High
0
Medium
0
Low
3
Informational
1
Security analysis | Risk level | Remediation Date |
---|---|---|
BALANCE CHECK MISSING WHEN EASY MODE IN ADD LIQUIDITY | Low | Risk Accepted |
INSUFFICIENT VERIFICATION OF REFUND AMOUNTS IN ADD LIQUIDITY | Low | Risk Accepted |
USERS REWARDS LOSS IF FARM SET START REWARDS AFTER DEPOSITS | Low | Risk Accepted |
MULTIPLE TOKEN ACCOUNTS CHECK MISSING | Informational | Acknowledged |
// Low
The AddLiquidity
instruction allows the operation in easy mode by providing the is_easy_mode value as parameter to true. Additionally, the amount_0 and amount_1 values are provided. When easy mode is enabled, the liquidity addition values are derived from subtracting amount_0 and amount_1 from the amount of token_account_0 and token_account_1 accounts, respectively. However, this operation does not verify if the amount of token_account_0 and token_account_1 are greater than the provided amount_0 and amount_1 values. If not, this would result in a panic due to an overflow error.
add_liquidity.rs
pub fn handle_add_liquidity<'info>(
ctx: Context<'_, '_, '_, 'info, AddLiquidity<'info>>,
amount_0: u64,
amount_1: u64,
is_base_amount0: bool,
is_easy_mode: bool,
deposit_fee_rate: u64,
) -> Result<u128> {
solana_program::log::sol_log("start");
let (amount_0, amount_1) = if is_easy_mode {
(
ctx.accounts.token_source_0.amount - amount_0,
ctx.accounts.token_source_1.amount - amount_1,
)
} else {
(amount_0, amount_1)
};
A similar issue arises when any user calls the Swap
instruction, which swaps or transfers funds from token_account_0 and token_account_1 to the wsol_vault. If someone then attempts to add liquidity using easy mode, and the token_account_0 and token_account_1 accounts lack sufficient funds, it will cause the same panic due to an overflow error as described until token_account_0 and token_account_1 have enough funds.
#[derive(Accounts)]
pub struct Swap<'info> {
#[account(signer)]
pub user: Signer<'info>,
#[account(seeds = [FARM_ROOT, b"CHEF_CONFIG"], bump)]
pub chef_config: Box<Account<'info, ChefConfig>>,
/// CHECK: authority account
#[account(seeds = [FARM_ROOT, b"AUTHORITY"], bump)]
pub authority: UncheckedAccount<'info>,
#[account(
seeds = [FARM_ROOT, b"POOL_CONFIG", token_mint_0.key().as_ref(), token_mint_1.key().as_ref()],
bump
)]
pub pool_config: Box<Account<'info, PoolConfig>>,
To address this issue, it is recommended to add a validation to check the amount of token_account_0 and token_account_1 is higher than the provided amount_0 and amount_1 values, if is_easy_mode.
RISK ACCEPTED: The Entangle team accepted the risk related to this finding.
// Low
The AddLiquidity
instruction, called by deposit
, calculates the corresponding amounts to increase liquidity based on the user-provided amount_0 and amount_1, considering the following:
Transfer fees that will be internally deducted during transfer_checked
.
fee0 and fee1, based on the deposit_fee_rate of the pool, which will be charged for adding liquidity
Once calculated, the liquidity is increased accordingly. If there is an excess amount between what was transferred to token_account_0 and token_account_1 and the amount used to increase liquidity after deducting the mentioned fees, this excess is refunded to the user as refund0 and refund1.
If either of the calculated refund values (refund0
or refund1
) is greater than zero but less than the transfer fee deducted during TransferChecked
, it will panic due to an overflow error. This is due to a lack of verification in the instruction handler, as the transfer fee will be subtracted from the intended refund amount.
add_liquidity.rs
ctx.accounts.token_account_0.reload()?;
ctx.accounts.token_account_1.reload()?;
let vault0_after_deposit = ctx.accounts.token_account_0.amount;
let vault1_after_deposit = ctx.accounts.token_account_1.amount;
let leftover0 = vault0_after_deposit - vault0_before_transfer;
let leftover1 = vault1_after_deposit - vault1_before_transfer;
solana_program::log::sol_log(&format!(
"leftover0: {} leftover1: {}",
leftover0, leftover1
));
let refund0 = if leftover0 > fee0 {
leftover0 - fee0
} else {
0
};
let refund1 = if leftover1 > fee1 {
leftover1 - fee1
} else {
0
};
solana_program::log::sol_log(&format!("refund0: {} refund1: {}", refund0, refund1));
if refund0 > 0 {
// Transfer token 0
let accounts = TransferChecked {
from: ctx.accounts.token_account_0.to_account_info(),
mint: ctx.accounts.token_mint_0.to_account_info(),
to: ctx.accounts.token_source_0.to_account_info(),
authority: ctx.accounts.authority.to_account_info(),
};
let cpi = CpiContext::new_with_signer(ctx.accounts.get_token_program_0(), accounts, seeds);
transfer_checked(cpi, refund0, ctx.accounts.token_mint_0.decimals)?;
}
if refund1 > 0 {
// Transfer token 1
let accounts = TransferChecked {
from: ctx.accounts.token_account_1.to_account_info(),
mint: ctx.accounts.token_mint_1.to_account_info(),
to: ctx.accounts.token_source_1.to_account_info(),
authority: ctx.accounts.authority.to_account_info(),
};
let cpi = CpiContext::new_with_signer(ctx.accounts.get_token_program_1(), accounts, seeds);
transfer_checked(cpi, refund1, ctx.accounts.token_mint_1.decimals)?;
}
To address this issue, it is recommended to add to the validation a check that verifies the amount to be transferred as refund is greater than zero and is also greater than the corresponding transfer fee, if it is any.
RISK ACCEPTED: The Entangle team accepted the risk related to this finding.
// Low
During the initialization of the FarmState, the start_time is assigned a default value of zero. If this value is not modified before users make deposits, it will create the impression that the FarmState has started, allowing deposit to be performed and rewards to be obtained.
impl FarmState {
pub const LEN: usize = 8 + 32 + 8 * 7;
pub fn new(config: FarmConfig) -> Self {
Self {
admin: config.admin,
xborpa_cooldown: config.xborpa_cooldown,
xborpa_redeem_fee: config.xborpa_redeem_fee,
xborpa_haircut: config.xborpa_haircut,
total_borpa_minted: 0,
total_xborpa: 0,
allocated_reward_share: 0,
start_time: 0,
}
}
When users make their first deposits, their new positions will be initialized through the Harvest
instruction, updating their last_claim_timestamp to the current unix_timestamp. The Harvest
instruction handles the necessary calculations for users to receive rewards based on the last_claim_timestamp of their position at the time of the call.
However, the SetStartRewards
instruction allows the admin to modify the start_time at any moment.
set_start_rewards.rs
pub fn handle_set_start_rewards(ctx: Context<SetStartRewards>, start_time: u64) -> Result<()> {
ctx.accounts
.farm_state
.set_start_rewards(start_time, ctx.accounts.clock.unix_timestamp as u64)?;
Ok(())
pub fn set_start_rewards(
&mut self,
start_time: u64,
now: u64,
) -> std::result::Result<(), CustomError> {
/*if self.start_time != 0 && now > self.start_time {
return Err(CustomError::RewardsAlreadyStarted);
}*/
if now > start_time && start_time != 0 {
return Err(CustomError::InvalidStartTime);
}
self.start_time = start_time;
Ok(())
If this value is changed to postpone the start_time after deposits have been made, users who have previously deposited will lose the rewards corresponding to the days that have passed since they deposited.This occurs because rewards cannot be calculated until the Farm has started. Once the Farm begins, the last_claim_timestamp
will be updated based on the time of the call.
harvest.rs
if !self.farm_state.is_started(self.clock.unix_timestamp as u64) {
return Ok(());
}
if self.position.last_claim_timestamp == 0
|| self.position.last_claim_timestamp < self.farm_state.start_time
{
self.position.last_claim_timestamp = self.clock.unix_timestamp as u64;
return Ok(());
}
To address this issue, consider implementing one of the following options:
Disallow deposits if the Farm has is not started.
Prevent changes to the start time of the Farm State once deposits have been made.
RISK ACCEPTED: The Entangle team accepted the risk related to this finding.
// Informational
The Harvest
instruction requires several accounts to be provided, the borpa_target_vault among them, where the amount of borpa rewards will be minted to. However, it is not validated that the authority of the account matches that of the signing user, allowing any borpa token account to be provided.
/// Target vault to claim $BORPA to
#[account(mut)]
pub borpa_target_vault: Box<InterfaceAccount<'info, TokenAccount>>,
let accounts = MintBorpa {
mint_authority: self.authority.to_account_info(),
authority: self.borpa_core_authority.to_account_info(),
config: self.borpa_core_config.to_account_info(),
mint: self.borpa_mint.to_account_info(),
vault: self.borpa_target_vault.to_account_info(),
token_program: self.token_program_2022.to_account_info(),
};
let bump = &[authority_bump][..];
let seed = &[FARM_ROOT, b"AUTHORITY", bump][..];
let seeds = &[seed];
let cpi = CpiContext::new_with_signer(self.borpa_core.to_account_info(), accounts, seeds);
self.farm_state.register_mint(borpa)?;
mint_borpa(cpi, borpa)?;
Consider adding the missing validation as a best practice to ensure that borpa rewards are minted in the user's borpa token account if expected.
ACKNOWLEDGED: The Entangle team has acknowledged this finding.
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 |
RUSTSEC-2024-0344 | curve25519-dalek | Timing variability in |
RUSTSEC-2024-0336 | rustls |
|
RUSTSEC-2023-0065 | tungstenite | Tungstenite allows remote attackers to cause a denial of service |
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.
// Download the full report
* Use Google Chrome for best results
** Check "Background Graphics" in the print settings if needed