Prepared by:
HALBORN
Last Updated 07/17/2024
Date of Engagement by: June 4th, 2024 - June 12th, 2024
100% of all REPORTED Findings have been addressed
All findings
5
Critical
0
High
0
Medium
1
Low
2
Informational
2
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.
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.
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
).
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.
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
1
Low
2
Informational
2
Security analysis | Risk level | Remediation Date |
---|---|---|
Slippage values should be enforced | Medium | Risk Accepted |
Multiple unsafe arithmetic operations | Low | Risk Accepted |
Lack of Two-Step admin transfer mechanism | Low | Risk Accepted |
Solana and Anchor versions are not current | Informational | Acknowledged |
Multiple input validations are missing | Informational | Acknowledged |
// Medium
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.
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
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.
RISK ACCEPTED: The Entangle team accepted the risk of this finding.
// Low
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;
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.
RISK ACCEPTED: The Entangle team accepted the risk of this finding.
// Low
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;
}
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.
RISK ACCEPTED: The Entangle team accepted the risk of this finding.
// Informational
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"
It is recommended to update dependencies to their current versions, as specified:
Solana: v1.18.15
Anchor: v0.30.0
ACKNOWLEDGED: The Entangle team acknowledged this finding.
// Informational
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.
It is recommended to perform input validation over the parameters that are given to administrative instructions.
ACKNOWLEDGED: The Entangle team acknowledged this finding.
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 |
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.
// Download the full report
* Use Google Chrome for best results
** Check "Background Graphics" in the print settings if needed