Prepared by:
HALBORN
Last Updated 12/17/2024
Date of Engagement: November 19th, 2024 - November 28th, 2024
100% of all REPORTED Findings have been addressed
All findings
4
Critical
0
High
0
Medium
0
Low
2
Informational
2
Shuttle labs
engaged Halborn to conduct a security assessment on their Solana Validator program beginning on November 19th, 2024 and ending on November 28th, 2024. The security assessment was scoped to the smart contracts provided in the GitHub repository genius-contracts-solana, commit hashes, and further details can be found in the Scope section of this report.
The Shuttle labs
a new version of the Genius
program. This new version uses orchestrators to manage most of the needed functions to operate in the Genius
ecosystem.
Halborn was provided 8 days for the engagement and assigned one full-time security engineer to review the security of the Solana Programs in scope. The engineer is a blockchain and smart contract security expert with advanced smart contract hacking skills, and deep knowledge of multiple blockchain protocols.
The purpose of the assessment is to:
Identify potential security issues within the codebase.
Validate that the user's funds are safe
Verify that the logic implemented by Shuttle Labs
does not introduce any vulnerability
In summary, Halborn identified some improvements to reduce the likelihood and impact of multiple risks, which were partially addressed by the Shuttle Labs team
. The main ones were the following:
Implement the conf parameter to calculate the USDC value according to Pyth documentation.
Add a validation in create_order to check that both source and destination chains are different.
Halborn performed a combination of manual review and security testing based on scripts to balance efficiency, timeliness, practicality, and accuracy in regard to the scope of this assessment. While manual testing is recommended to uncover flaws in logic, process, and implementation; automated testing techniques help enhance coverage of the code and can quickly identify items that do not follow the security best practices. The following phases and associated tools were used during the assessment:
Research into architecture and purpose.
Differences analysis using GitLens to have a proper view of the differences between the mentioned commits
Graphing out functionality and programs logic/connectivity/functions along with state changes
EXPLOITABILITY 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
2
Informational
2
Security analysis | Risk level | Remediation Date |
---|---|---|
Incorrect usage of Pyth price data without considering confidence interval | Low | Solved - 12/03/2024 |
Orchestrators have full access to bridge funds | Low | Risk Accepted - 12/16/2024 |
Users cannot cancel token bridge without orchestrator consent | Informational | Acknowledged - 12/16/2024 |
Missing validation for src_chain_id and dest_chain_id in create_order | Informational | Solved - 12/03/2024 |
//
The create_order
function is responsible for creating a new order in the program. It performs several critical tasks, including:
Validating the stability of the USDC price obtained from the Pyth Network.
Ensuring that sufficient fees are provided for cross-chain operations.
Initializing and populating the order with details such as the source and destination chains, token amounts, and involved parties.
Transferring USDC from the user’s token account to a vault account for order processing.
During the price validation step, the function fetches the USDC price using the Pyth price feed and adjusts it based on the feed's exponent
value:
create_order.rs
:
msg!("deposit USDC amount: {:?}", amount);
let price_update = &mut ctx.accounts.price_update;
let feed_id: [u8; 32] = get_feed_id_from_hex(FEED_ID)?;
let price = price_update.get_price_no_older_than(&Clock::get()?, MAXIMUM_AGE, &feed_id)?;
// Adjust price to floating-point by scaling with 10^exponent
let adjusted_price: f64 = (price.price as f64) * 10f64.powi(price.exponent);
However, this implementation does not consider the conf
(confidence interval) parameter provided by Pyth, which represents the uncertainty range in the reported price. Ignoring conf
might lead to decisions based on potentially unreliable price data, especially during periods of high market volatility.
By not incorporating the conf
parameter, the program exposes itself to risks where the USDC/USD price might appear stable but has significant uncertainty. This could lead to:
Depeg Exploitation: If USDC experiences a depeg and the confidence interval (conf
) is not considered, the price could appear valid while being inaccurate due to high uncertainty. Users on the Solana network could exploit this by exchanging depegged USDC tokens on Solana for more valuable tokens on another network, effectively transferring the depeg losses to the program and its users.
Systemic Risks Across Chains: As the function facilitates cross-chain operations, overlooking the confidence interval might propagate incorrect exchange rates between networks, leading to financial imbalances or exploits.
Inaccurate Price Validation: In volatile market conditions, large conf
values signal unreliable data. Ignoring this parameter leaves the program vulnerable to decisions based on incomplete or misleading price information.
Modify the price validation logic to incorporate the conf
parameter. This ensures that the price used for validation is reliable and falls within an acceptable range. For example:
create_order.rs
:
let lower_bound = (price.price - price.conf) as f64 * 10f64.powi(price.exponent);
let upper_bound = (price.price + price.conf) as f64 * 10f64.powi(price.exponent);
// Validate price bounds
require!(lower_bound > 0.99, GeniusError::StableCoinPriceTooLow);
require!(upper_bound < MAX_ACCEPTABLE_PRICE, GeniusError::StableCoinPriceTooHigh);
By incorporating the confidence interval, the program can assess the validity of the price more accurately, mitigating risks associated with volatile or unreliable price data.
SOLVED: The Genius team solved the issue by adding the conf parameter to the calculation.
//
In the Genius protocol, orchestrators are considered trusted actors, as they are controlled by the Genius team itself. Additionally, orchestrators can only be added to the system by an admin of the platform, ensuring they are pre-approved entities.
The program includes two functions, fill_order
and remove_bridge_liquidity
, that grant orchestrators direct access to the bridge’s funds stored in the vault:
fill_order
: This function allows orchestrators to withdraw USDC from the vault and transfer it to their own associated token accounts. Genius has declared that this function is intended to be executed via a Jito bundle, which would include an additional transaction transferring the funds from the orchestrator to the user. However, there is no restriction in place to enforce the use of Jito bundles, meaning an orchestrator could call fill_order
independently and retain the funds without transferring them to the intended user. Furthermore, orchestrators have absolute control over the parameters of the order, including the amount to withdraw, the trader’s address, and the fees to be applied. This provides them with full discretion to manipulate the terms of each transaction.
remove_bridge_liquidity
: This function enables orchestrators to directly remove USDC liquidity from the bridge. It is primarily designed for managing the bridge’s reserves, but it similarly provides unrestricted withdrawal access.
Both functions can be seen in the snippets below:
fill_order.rs
:
impl FillOrder<'_> {
pub fn process_instruction(
ctx: Context<Self>,
amount: u64,
seed: [u8; 32],
trader: [u8; 32],
src_chain_id: u32,
dest_chain_id: u32,
token_in: [u8; 32],
fee: u64,
min_amount_out: u64,
) -> Result<()> {
msg!("withdraw USDC amount: {:?}", amount);
// check orchestrator is eligible
let orchestrator_state = &ctx.accounts.orchestrator_state;
require!(
orchestrator_state.authorized == true,
GeniusError::IllegalOrchestrator
);
if ctx.accounts.order.status != OrderStatus::Unexistant {
return err!(GeniusError::OrderAlreadyExists);
}
let order = &mut ctx.accounts.order;
order.amount_in = amount;
order.seed = seed;
order.trader = trader;
order.receiver = ctx.accounts.receiver.key().to_bytes();
order.src_chain_id = src_chain_id;
order.dest_chain_id = dest_chain_id;
order.token_in = token_in;
order.fee = fee;
order.status = OrderStatus::Filled;
order.min_amount_out = min_amount_out;
order.token_out = ctx.accounts.token_out.key().to_bytes();
let global_state = &ctx.accounts.global_state;
let current_balance = get_vault_balance(&ctx.accounts.ata_vault)?;
let withdraw_amount = amount - order.fee;
require!(
balance_within_threshold(
current_balance,
current_balance - withdraw_amount,
global_state.rebalance_threshold,
ctx.accounts.asset.reserved_fees
),
GeniusError::NeedRebalance
);
// Transfer USDC from vault to orchestrator
token_transfer_with_signer(
ctx.accounts.ata_vault.to_account_info().clone(),
ctx.accounts.vault.to_account_info().clone(),
ctx.accounts.ata_orchestrator.to_account_info().clone(),
ctx.accounts.token_program.to_account_info().clone(),
&[&[VAULT_SEED, &[ctx.bumps.vault]]],
withdraw_amount,
)
}
}
remove_bridge_liquidity.rs
:
impl RemoveBridgeLiquidity<'_> {
pub fn process_instruction(ctx: Context<Self>, amount: u64) -> Result<()> {
let orchestrator_state = &ctx.accounts.orchestrator_state;
require!(
orchestrator_state.authorized == true,
GeniusError::IllegalOrchestrator
);
let current_balance = get_vault_balance(&ctx.accounts.ata_vault)?;
require!(
balance_within_threshold(
current_balance,
current_balance
.checked_sub(amount)
.ok_or(GeniusError::InvalidAmount)?,
ctx.accounts.global_state.rebalance_threshold,
ctx.accounts.asset.reserved_fees
),
GeniusError::NeedRebalance
);
// Transfer USDC from vault to orchestrator
token_transfer_with_signer(
ctx.accounts.ata_vault.to_account_info().clone(),
ctx.accounts.vault.to_account_info().clone(),
ctx.accounts.ata_orchestrator.to_account_info().clone(),
ctx.accounts.token_program.to_account_info().clone(),
&[&[VAULT_SEED, &[ctx.bumps.vault]]],
amount,
)
}
}
Despite the safeguards of trusted orchestrator designation and admin-only additions, concentrating full control of funds into the orchestrators’ hands introduces systemic risk if trust is compromised or if an orchestrator account is exploited.
We can mention the next results as impact for the system:
Malicious orchestrators: A rogue or compromised orchestrator could withdraw all funds, leading to a complete loss of bridge liquidity.
Operational risk: Unintentional errors by orchestrators, such as withdrawing excessive funds, could disrupt the bridge's ability to process user transactions.
Single point of failure: A central point of control over funds introduces a high-risk vector for attacks or misuse.
Jito bundle bypass: The lack of enforcement for Jito bundles means orchestrators could misuse the fill_order
function to retain funds without completing the intended user transfer.
Loss of user trust: A significant breach or misuse could cause reputational damage to the protocol and reduce user confidence in the system.
To mitigate the risks mentioned above, consider implementing all of the following solutions:
Fill order and transfer to user in the same transaction: Bundle multiple transactions in the Jito Bundle, as initially expected. In one of the transactions, include 2 instructions. The first instruction is fill_order
and the next instruction will transfer the corresponding tokens from the Orchestrator to the final user. Additionally, modify the fill_order
instruction to require that the next instruction is effectively an instruction that transfers an amount > min_amount_out
of token_out
tokens to the final user. This way, the Orchestrator can't use fill_order
without transferring the tokens to the user.
Cross-Chain Messaging System: To mitigate the risk associated with orchestrators having complete control over the parameters of the orders in the fill_order
function, consider implementing a cross-chain messaging protocol, such as Wormhole, that can handle order parameters off-chain in a verifiable and secure manner. By utilizing cross-chain messaging, you can decouple critical order parameters from orchestrator discretion, ensuring that the parameters are agreed upon transparently and independently across different chains, reducing the risk of manipulation.
Withdrawal Limits: Implement limits on the amount that can be withdrawn in a single transaction or within a specific time period.
Enhanced Monitoring: Add on-chain monitoring and alerting for large withdrawals to notify stakeholders immediately.
Audit Trails: Maintain detailed logs of orchestrator actions on-chain to provide transparency and accountability. These logs should include withdrawal amounts, timestamps, and orchestrator identities.
Periodic Reviews: Conduct periodic reviews of orchestrator activity and ensure compliance with protocol policies.
By implementing these measures, Genius can reduce the risks associated with centralized fund access, enforce the proper usage of fill_order
, and maintain user trust and system integrity.
RISK ACCEPTED: The Genius team accepted the risk of this finding.
//
In the Genius protocol, users rely on orchestrators to manage the cancellation of token bridge transactions. The WithdrawStableCoin
function, which is part of the bridge liquidity system, is governed by the orchestrator. Users who wish to cancel or withdraw tokens via the bridge cannot do so independently. They are required to have the orchestrator's consent for the transaction to proceed.
The mentioned situation can be seen in the snippet below, where we can see that both signatures from user and orchestrator are required, and that the orchestrator state is derived from the orchestrator account:
#[derive(Accounts)]
pub struct WithdrawStableCoin<'info> {
// fee payer
#[account(mut)]
pub user: Signer<'info>,
// need to check eligible withdraw
pub orchestrator: Signer<'info>,
// Global state
#[account(
seeds = [GLOBAL_SEED],
bump,
constraint = global_state.frozen == false @GeniusError::GlobalStateFrozen
)]
pub global_state: Box<Account<'info, GlobalState>>,
// Asset
#[account(
seeds = [ASSET_SEED],
bump,
)]
pub asset: Box<Account<'info, Asset>>,
// Stores orchestrator info
#[account(
seeds = [orchestrator.key().as_ref(), ORCHESTRATOR_SEED],
bump,
)]
pub orchestrator_state: Box<Account<'info, OrchestratorState>>,
// Needed to check vault authority
#[account(
seeds = [VAULT_SEED],
bump,
)]
/// CHECK: This is not dangerous because we don't read or write from this account
pub vault: AccountInfo<'info>,
// USDC ata of vault
#[account(
mut,
associated_token::mint = usdc_mint,
// Authority is set to vault
associated_token::authority = vault,
)]
pub ata_vault: Box<Account<'info, TokenAccount>>,
// destination token account of user
#[account(
init_if_needed,
payer = user,
associated_token::mint = usdc_mint,
associated_token::authority = user,
)]
pub ata_user: Box<Account<'info, TokenAccount>>,
// The mint of $USDC because it's needed from above ⬆ token::mint = ...
#[account(
address = global_state.base_mint,
)]
pub usdc_mint: Box<Account<'info, Mint>>,
pub token_program: Program<'info, Token>,
pub associated_token_program: Program<'info, AssociatedToken>,
pub system_program: Program<'info, System>,
}
impl WithdrawStableCoin<'_> {
pub fn process_instruction(ctx: Context<Self>, amount: u64) -> Result<()> {
msg!("withdraw usdc amount: {:?}", amount);
// check orchestrator is eligible
let orchestrator_state = &ctx.accounts.orchestrator_state;
require!(
orchestrator_state.authorized == true,
GeniusError::IllegalOrchestrator
);
let global_state = &ctx.accounts.global_state;
let current_balance = get_vault_balance(&ctx.accounts.ata_vault)?;
require!(
balance_within_threshold(
current_balance,
current_balance - amount,
global_state.rebalance_threshold,
ctx.accounts.asset.reserved_fees
),
GeniusError::NeedRebalance
);
// Transfer USDC from vault to user
token_transfer_with_signer(
ctx.accounts.ata_vault.to_account_info().clone(),
ctx.accounts.vault.to_account_info().clone(),
ctx.accounts.ata_user.to_account_info().clone(),
ctx.accounts.token_program.to_account_info().clone(),
&[&[VAULT_SEED, &[ctx.bumps.vault]]],
amount,
)
}
}
This creates a scenario where users have no control over their token bridge transactions once initiated. The orchestrator is the only entity that can approve or cancel these transactions, placing complete control in their hands. This means that if a user decides to cancel a transaction or modify the bridge transaction, the orchestrator’s authorization is mandatory.
The orchestrator's power to approve or reject token bridge cancellations exposes users to potential risks, such as:
Delays in transaction processing: Users are dependent on the orchestrator to approve or cancel their bridge transactions, and delays from the orchestrator could result in missed opportunities or unwanted transactions.
Centralization of control: Since the orchestrator holds exclusive control over bridge cancellations, this introduces a centralized point of failure. If the orchestrator is unavailable or malicious, users could lose the ability to cancel or modify transaction
To mitigate the mentioned risks, consider the following improvements:
Automated cancellation mechanism: Introduce an automatic cancellation mechanism for token bridges that can be triggered by specific conditions, reducing reliance on orchestrator approval for cancellations.
Increased transparency and auditing: Implement logging and auditing of all bridge transactions, including cancellation requests, to provide transparency about orchestrator actions and the reasons behind transaction rejections.
Clear user communication: Notify users when their cancellation requests are pending approval, providing an estimated timeframe for the orchestrator's response and clear messaging regarding the status of their transactions.
Backup orchestrators and failover protocols: Ensure the availability of backup orchestrators to approve or cancel transactions in case the primary orchestrator becomes unavailable or is unresponsive.
By decentralizing control over token bridge transactions, improving transparency, and ensuring failover mechanisms, Genius can reduce the risks associated with users' inability to independently cancel or modify token bridge transactions.
ACKNOWLEDGED: The Genius team acknowledged this finding.
//
The create_order
function is responsible for creating a new order for a user to transfer tokens across different blockchains. It checks the deposit amount, verifies that the user has sufficient fees, and sets the details for the order, including the source and destination chains (src_chain_id
and dest_chain_id
). However, the function does not verify that the source and destination chains are distinct. As we can see in the snippet below, the values for src_chain_id
and dest_chain_id
are directly assigned from the function parameters without any validation:
pub fn process_instruction(
ctx: Context<Self>,
amount: u64,
seed: [u8; 32],
trader: [u8; 32],
receiver: [u8; 32],
src_chain_id: u32,
dest_chain_id: u32,
token_in: [u8; 32],
fee: u64,
min_amount_out: u64,
token_out: [u8; 32],
) -> Result<()> {
msg!("deposit USDC amount: {:?}", amount);
let price_update = &mut ctx.accounts.price_update;
let feed_id: [u8; 32] = get_feed_id_from_hex(FEED_ID)?;
let price = price_update.get_price_no_older_than(&Clock::get()?, MAXIMUM_AGE, &feed_id)?;
// Adjust price to floating-point by scaling with 10^exponent
let adjusted_price: f64 = (price.price as f64) * 10f64.powi(price.exponent);
require!(adjusted_price > 0.99, GeniusError::StableCoinPriceTooLow);
if ctx.accounts.order.status != OrderStatus::Unexistant {
return err!(GeniusError::OrderAlreadyExists);
}
let min_fee = ctx.accounts.target_chain_min_fee.min_fee;
if min_fee > fee {
return err!(GeniusError::InsufficientFees);
}
ctx.accounts.order.amount_in = amount;
ctx.accounts.order.seed = seed;
ctx.accounts.order.trader = trader;
ctx.accounts.order.receiver = receiver;
ctx.accounts.order.src_chain_id = src_chain_id;
ctx.accounts.order.dest_chain_id = dest_chain_id;
ctx.accounts.order.token_in = token_in;
ctx.accounts.order.fee = fee;
ctx.accounts.order.status = OrderStatus::Created;
ctx.accounts.order.min_amount_out = min_amount_out;
ctx.accounts.order.token_out = token_out;
ctx.accounts.asset.unclaimed_fees += fee;
let signer = &ctx.accounts.signer;
let orchestrator = &ctx.accounts.orchestrator;
let orchestrator_state = &ctx.accounts.orchestrator_state;
// Convert signer key and orchestrator key to [u8; 32] for comparison
let signer_key_bytes: [u8; 32] = signer.key().to_bytes();
let orchestrator_key_bytes: [u8; 32] = orchestrator.key().to_bytes();
require!(
signer_key_bytes == trader || signer_key_bytes == orchestrator_key_bytes,
GeniusError::UnauthorizedSigner
);
require!(
orchestrator_state.authorized == true,
GeniusError::IllegalOrchestrator
);
// Transfer USDC from orchestrator to vault
token_transfer_user(
ctx.accounts.ata_signer.to_account_info().clone(),
ctx.accounts.signer.to_account_info().clone(),
ctx.accounts.ata_vault.to_account_info().clone(),
ctx.accounts.token_program.to_account_info().clone(),
amount,
)
}
Without checking that the source and destination chains are different, the user may end up paying fees for a transaction that is essentially redundant, leading to unnecessary costs. This could be especially problematic in a system where chain-to-chain transfers are intended, and users may mistakenly believe their funds are being transferred across chains when they are not.
Consider adding a validation step that checks whether the src_chain_id
and dest_chain_id
are distinct. If they are the same, the function should reject the transaction or notify the user about the issue.
SOLVED: The Genius team solved the issue by adding a check to validate that both chains are different.
Halborn used automated security scanners to assist with the 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 reviewers 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.
ID | package | Short Description |
---|---|---|
RUSTSEC-2022-0093 | ed25519-dalek | Double Public Key Signing Function Oracle Attack on |
RUSTSEC-2024-0344 | curve25519-dalek | Timing variability in |
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