Halborn Logo

icon

Waterusdc and Vaultka Solana Programs - Vaultka


Prepared by:

Halborn Logo

HALBORN

Last Updated 08/27/2024

Date of Engagement by: July 22nd, 2024 - August 19th, 2024

Summary

100% of all REPORTED Findings have been addressed

All findings

9

Critical

1

High

1

Medium

2

Low

3

Informational

2


1. Introduction

Vaultka engaged Halborn to conduct a security assessment on their Waterusdc and Vaultka Solana programs beginning on July 22, 2024, and ending on August 19, 2024. The security assessment was scoped to the Solana Programs provided in vaultkarust GitHub repository. Commit hashes and further details can be found in the Scope section of this report.

The Waterusdc program is a single side lending pool program that allows whitelisted users or other programs to borrow USDC tokens from the lending pool. The Vaultkausdc program is a strategy program allowing users to borrow USDC tokens via the Waterusdc program and gain exposure to the Jupiter's JLP token. The program uses the Pyth oracle in order to fetch current JLP and USDC prices, and also allows users to use leverage up to certain limits to increase market exposure.

2. Assessment Summary

Halborn was provided 4 weeks 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 Solana Programs.

    • Ensure that smart contract functionality operates as intended.

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

    • Withdraw fee is transferred to the user instead of the fee vault

    • Inefficient slippage control

    • Incorrect token price conversion prevent withdrawal

    • Incorrect accounts mutability

Only informational issues were currently only acknowledged and not addressed.


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 anchor testing (`anchor test`)

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: vaultkarust
(b) Assessed Commit ID: 6e81ea1
(c) Items in scope:
  • /vaultkarust/blob/fixes-tests/Anchor.toml
  • /vaultkarust/blob/fixes-tests/Cargo.toml
  • /vaultkarust/blob/fixes-tests/programs/vaultkausdc/Cargo.toml
↓ Expand ↓
Out-of-Scope: /vaultkarust/blob/fixes-tests/programs/watersol/Cargo.toml, /vaultkarust/blob/fixes-tests/programs/watersol/Xargo.toml, /vaultkarust/blob/fixes-tests/programs/watersol/src/lib.rs, /vaultkarust/blob/fixes-tests/migrations/deployUSDCStrategy.js, /vaultkarust/blob/fixes-tests/migrations/deployWaterUSDC.js, /vaultkarust/blob/fixes-tests/programs/vaultkasol/Cargo.toml, /vaultkarust/blob/fixes-tests/programs/vaultkasol/Xargo.toml, /vaultkarust/blob/fixes-tests/programs/vaultkasol/src/lib.rs, /vaultkarust/blob/fixes-tests/package.json, /vaultkarust/blob/fixes-tests/tsconfig.json
Remediation Commit ID:
Out-of-Scope: New features/implementations after the remediation commit IDs.

6. Assessment Summary & Findings Overview

Critical

1

High

1

Medium

2

Low

3

Informational

2

Security analysisRisk levelRemediation Date
WITHDRAW FEE IS TRANSFERRED TO THE USER INSTEAD OF THE FEE VAULTCriticalSolved - 08/21/2024
INEFFICIENT SLIPPAGE CONTROLHighSolved - 08/26/2024
INCORRECT ACCOUNTS MUTABILITYMediumSolved - 08/23/2024
INCORRECT TOKEN PRICE CONVERSION PREVENTS WITHDRAWALMediumSolved - 08/21/2024
RISK OF OUTDATED PRICE FEEDLowSolved - 08/21/2024
SET_JLP_PRICE INSTRUCTION WILL ALWAYS FAILLowSolved - 08/21/2024
UNBOUNDED FEES CALCULATIONLowSolved - 08/21/2024
INABILITY TO CLOSE UNNEEDED ACCOUNTSInformationalAcknowledged
FORMAL ISSUES AND RECOMMENDATIONSInformationalAcknowledged

7. Findings & Tech Details

7.1 WITHDRAW FEE IS TRANSFERRED TO THE USER INSTEAD OF THE FEE VAULT

// Critical

Description

The Withdraw instruction of the Waterusdc program allows users to withdraw liquidity from the lending program for a predefined fee.

However, instead of transferring the fee to the fee vault as intended, the instruction mistakenly returns the fee to the user.

As a result, the protocol is unable to collect fees from users.


waterusdc/src/lib.rs

let fee_transfer_accounts = TransferChecked {
            from: ctx.accounts.usdc_token_account.to_account_info(),
            to: ctx.accounts.user_ata.to_account_info(), 
            authority: ctx.accounts.program_authority.to_account_info(),
            mint: ctx.accounts.usdc_mint.to_account_info(),
        };
let fee_amount_context = CpiContext::new_with_signer(
            ctx.accounts.token_program.to_account_info(),
            fee_transfer_accounts,
            signer_seeds,
        );
anchor_spl::token::transfer_checked(fee_amount_context, w_fee, 6)?;

Proof of Concept
  1. Initialize waterusdc program

  2. Deposit USDC tokens

  3. Withdraw USDC tokens


    Withdraw instruction testWithdraw instruction logsFailed test
BVSS
Recommendation

To address this issue, it is recommended to correct the destination address so that the fee is sent to the fee vault account.


Remediation

SOLVED: The Vaultka team solved the issue by modifying the destination address and setting it to the correct fee vault account address.

Remediation Hash

7.2 INEFFICIENT SLIPPAGE CONTROL

// High

Description

Both the ExecuteDeposit and ExecuteWithdraw instructions ensure that the received amount falls within the slippage range defined by the expected amount and slippage percentage, verifying that there hasn't been a significant exchange rate change between the request and execution.

However, the RequestDeposit and RequestWithdraw instructions do not allow users to set slippage limits. In volatile markets, outdated Pyth oracle prices, or incorrect manual JLP price settings, users might request a deposit or withdrawal that results in an unexpected output amount.

Depending on the slippage control settings in the ExecuteDeposit or ExecuteWithdraw instructions, the execution might fail, causing funds to become stuck, or it might succeed, with the user receiving less than expected.

In addition to the issue above, the amount in the ExecuteDeposit or ExecuteWithdraw instructions is verified only using the slippage control. However, the slippage control parameter can be adjusted by the admin user without any bounds. Invoking the ExecuteDeposit instruction with an incorrect amount and large slippage range can result in loses either for the user or the protocol.

Proof of Concept
  1. Initialize the vaultkausdc and waterusdc programs

  2. Set strategy parameters

  3. Manually set the JLP token price to twice of the current price to simulate incorrect/outdated/volatile price

  4. Request deposit

  5. Set back the JLP token price to the current price

  6. Execute deposit

  7. Manually set the JLP token price to twice of the current price to simulate incorrect/outdated/volatile price

  8. Set back the JLP token price to the current price

  9. Execute deposit

Deposit request with incorrect price:

Deposit request with incorrect price

Execute deposit with correct price fails and with incorrect price completes successfully:

Execute deposit with correct price fails and with incorrect price passes
BVSS
Recommendation

To address this issue, it is recommended to implement slippage control also for the RequestDeposit and RequestWithdraw instructions and limit the allowed range of slippage.


Remediation

SOLVED: The Vaultka team solved the issue by adding a new input parameter to the RequestDeposit, RequestIncreaseCollateral and RequestWithdraw instructions so that the user is able to set the minimal output amount that he or she can accept. If the calculated output amount is less than the expected minimum, the instruction fails and the user has to repeat the request.

Remediation Hash

7.3 INCORRECT ACCOUNTS MUTABILITY

// Medium

Description

The vaultkausdc program implements manually the AccountsMeta structures, that are used during CPI (cross-program invocation) of Borrow and Repay instructions of the waterusdc program.

However, these AccountsMeta structs are implemented incorrectly and the token_program and usdc_mint accounts are marked as mutable instead of read-only.

This prevents the RequestDeposit and the ExecuteWithdraw instructions from being invoked due to cross-program invocation error.

vaultkausdc/src/lib.rs

impl<'info> ToAccountMetas for Borrow<'info> {
    fn to_account_metas(&self, _is_signer: Option<bool>) -> Vec<AccountMeta> {
        vec![
            AccountMeta::new(*self.borrower.key, true),
            AccountMeta::new(*self.vault.key, false),
            AccountMeta::new(*self.whitelisted_borrower.key, false),
            AccountMeta::new(*self.program_authority.key, false),
            AccountMeta::new(*self.receiver_ata.key, false),
            AccountMeta::new(*self.usdc_token_account.key, false),
            AccountMeta::new(*self.token_program.key, false), 
            AccountMeta::new(*self.usdc_mint.key, false), 
        ]
    }
}
impl<'info> ToAccountMetas for Repay<'info> {
    fn to_account_metas(&self, _is_signer: Option<bool>) -> Vec<AccountMeta> {
        vec![
            AccountMeta::new(*self.borrower.key, true),
            AccountMeta::new(*self.vault.key, false),
            AccountMeta::new(*self.whitelisted_borrower.key, false),
            AccountMeta::new(*self.program_authority_lending.key, false),
            AccountMeta::new(*self.usdc_token_account.key, false),
            AccountMeta::new(*self.borrower_ata.key, false),
            AccountMeta::new(*self.token_program.key, false), 
            AccountMeta::new(*self.usdc_mint.key, false) 
        ]
    }
}

In addition to this issue, the context structures Deposit, ExecuteDeposit, RequestIncreaseCollateral, RequestWithdraw and ExecuteWithdraw of the vaultkausdc program do not set correctly the mutability attribute of their accounts (some accounts are missing the mut attribute and are considered as read-only instead of mutable). It does not create a vulnerability in the program, however it does not allow the Anchor Typescript client to correctly generate the AccountMetas for the corresponding instruction. Therefore it is impossible to invoke these instructions using the generated Anchor Typescript client.

Proof of Concept
  1. Initialize the vaultkausdc and waterusdc programs

  2. Set strategy parameters

  3. Request deposit

  4. Execute deposit

  5. Request increase collateral

  6. Execute deposit

  7. Request withdrawal

  8. Execute withdrawal

RequestDeposit ix errorExecuteWithdraw ix error
BVSS
Recommendation

To address this issue, it is recommended to set the accounts token_program and usdc_mint as read-only. Also review the mutability attribute for each account with the Deposit, ExecuteDeposit, RequestIncreaseCollateral, RequestWithdraw and ExecuteWithdraw instruction context structs.

Remediation

SOLVED: The Vaultka team solved the issue by correctly setting the token_program and usdc_mint accounts as read-only and by correctly setting the mutability attribute to the corresponding accounts.

Remediation Hash

7.4 INCORRECT TOKEN PRICE CONVERSION PREVENTS WITHDRAWAL

// Medium

Description

The RequestWithdraw instruction allows users to initiate withdrawal and close their positions. The instruction calculates, among other calculations also the current price of JLP tokens in USDC tokens based on the price of JLP and USDC tokens in USD.

However, the price of JLP tokens is incorrectly scaled and is 10 times greater than it is supposed to be (it is multiplied by 10 twice instead of only once). This results in the JLP/USDC price being 10 times greater, and consequently also the resulting expected amount of USDC tokens is 10 times greater. This prevents the invocation of the ExecuteWithdraw instruction, because the received amount of USDC tokens will not be within the accepted range of allowed slippage.

vaultkausdc/src/lib.rs

let usdc_price = get_pyth_price_helper(pyth_price_account_usdc, strategy.usdc_usd_feed)?;
let mut jlp_price = 0;
if price_control.is_manual_price_update{
    jlp_price = price_control.jlp_price * 10; // here is the JLP price multiplied by 10
    msg!("Manual JLP price: {}", jlp_price);
} else {
    jlp_price = get_pyth_price_helper(pyth_price_account_jlp, strategy.jlp_sol_feed)? * 10; // here is the JLP price multiplied by 10
    msg!("Pyth JLP price: {}", jlp_price);
}
let mut jlp_p: u128 = jlp_price as u128;
jlp_p = jlp_p * 10; // here is the JLP price multiplied by 10 for the second time
msg!("JLP price: {}", jlp_p);
let mut usdc_p: u128 = usdc_price as u128;
usdc_p = usdc_p * 10;
msg!("USDC price: {}", usdc_p);
let jlp_price_usdc: u128 = jlp_p.checked_mul(10u128.pow(6)).ok_or(ErrorCode::ReceivedAmountOperationError)?
                    .checked_div(usdc_p).ok_or(ErrorCode::ReceivedAmountOperationError)?;
msg!("JLP price in USDC: {}", jlp_price_usdc);

pos_amount = pos_amount.checked_mul(10u64.pow(3)).ok_or(ErrorCode::ReceivedAmountOperationError)?;
let pa_a = pos_amount as u128;
let expected_amount_out_usdc: u128 = pa_a.checked_mul(jlp_price_usdc).ok_or(ErrorCode::ReceivedAmountOperationError)?
    .checked_div(10u128.pow(9)).ok_or(ErrorCode::ReceivedAmountOperationError)?;
Proof of Concept
  1. Initialize the vaultkausdc and waterusdc programs

  2. Set strategy parameters

  3. Request deposit

  4. Execute deposit

  5. Request withdrawal

  6. Manually execute withdrawal

RequestWithdraw and ExecuteWithdraw instruction logs.SlippageError
BVSS
Recommendation

To address this issue, it is recommended to correct the calculation of the JLP/USDC price pair.

Remediation

SOLVED: The Vaultka team solved the issue by removing the superfluous multiplication of the the JLP token price and thus corrected the price conversion.

Remediation Hash

7.5 RISK OF OUTDATED PRICE FEED

// Low

Description

The program relies on the Pyth oracle to retrieve the current USD prices of JLP and USDC tokens. However, the maximum age for the price feed update is currently set to 120,000 seconds (approximately 33.3 hours). If prices become outdated, such as during a Pyth oracle outage, and market volatility is high, both the user and the protocol risk losses due to incorrect exchange rates.

waterusdc/src/lib.rs

pub fn get_pyth_price_helper<'info>(price_account: &Account<'info, PriceUpdateV2>,feed_id: [u8; 32]) -> Result<u64> {
    let price_update = price_account;
    // get_price_no_older_than will fail if the price update is more than 30 seconds old
    let maximum_age: u64 = 120000; 
    // get_price_no_older_than will fail if the price update is for a different price feed.
    // This string is the id of the WIF/USD feed (close to JLP price). See https://pyth.network/developers/price-feed-ids for all available IDs.
    //let feed_id: [u8; 32] = get_feed_id_from_hex("0x4ca4beeca86f0d164160323817a4e42b10010a724c2217c6ee41b54cd4cc61fc")?;
    let price = price_update.get_price_no_older_than(&Clock::get()?, maximum_age, &feed_id)?;
    // Sample output:
    // The price is (7160106530699 ± 5129162301) * 10^-8
    msg!("The price is ({} ± {}) * 10^{}", price.price, price.conf, price.exponent);
    let final_price = price.price as u64;

    Ok(final_price)
}

BVSS
Recommendation

To address this issue, it is recommended to reduce the maximum age parameter.

Remediation

SOLVED: The Vaultka team solved the issue by decreasing the maximum age parameter to 120 seconds and thus reducing the risk of outdated price feed.

Remediation Hash

7.6 SET_JLP_PRICE INSTRUCTION WILL ALWAYS FAIL

// Low

Description

The SetJlpPrice instruction is intended to manually set the USD price of the JLP token by the admin and supposedly also by the authorized keeper.

However, the access control condition is flawed, as it always evaluates to true, leading to an IncorrectOwner error. Specifically, the code is checking whether the user.key is not equal to both admin.admin and strategy.keeper at the same time. This creates a situation where the condition will always be true, even if the user.key matches one of the two addresses, because a single user.key cannot simultaneously match both admin.admin and strategy.keeper.

waterusdc/src/lib.rs

// Set the JLP Price manually
pub fn set_jlp_price(ctx: Context<SetJLPPrice>, jlp_price: u64) -> ProgramResult {
    let strategy = &mut ctx.accounts.strategy;
    let admin = &ctx.accounts.admin;
    let price_control = &mut ctx.accounts.price_control;

    if *ctx.accounts.user.key != admin.admin || *ctx.accounts.user.key != strategy.keeper{
        return Err(ErrorCode::IncorrectOwner.into());
    }

    //MUST BE IN DECIMAL 8
    //ensure the new JLP price is not 0
    if jlp_price == 0 {
        return Err(ErrorCode::InvalidSetterData.into());
    }
    let min_jlp_price = price_control.jlp_price - (price_control.jlp_price * price_control.jlp_price_control_slippage / 100);
    let max_jlp_price = price_control.jlp_price + (price_control.jlp_price * price_control.jlp_price_control_slippage / 100);

    if jlp_price < min_jlp_price || jlp_price > max_jlp_price {
        return Err(ErrorCode::SlippageError.into());
    }

    price_control.jlp_price = jlp_price;

    Ok(())
}
BVSS
Recommendation

To address this issue, it is recommended to correct the access control condition. Either to authorize only one defined account (admin or keeper) or change the logical operator from OR to AND.

Remediation

SOLVED: The Vaultka team solved the issue by modifying the logical operator from OR to AND and thus authorizing the admin or keeper accounts to invoke the setJlpPrice instruction.

Remediation Hash

7.7 UNBOUNDED FEES CALCULATION

// Low

Description

During the ExecuteWithdraw instruction, the program deducts various fees from the user's profit during the withdrawal process. The amount of fees depends on the following parameters:

  • leverage

  • fixed_fee_split

  • mfee_percent

Although these parameters can be set within predefined limits, the resulting fees are not limited and certain parameter combinations may result in fees equal to or exceeding 100% of the profit. If the fees are less than or equal to 100% of the profit, the ExecuteWithdraw instruction will succeed, but the user risks receiving very little or no profit at all. If the fees exceed 100% of the profit, the ExecuteWithdraw instruction will fail due to overflow by subtraction, preventing the user from withdrawing their position.

waterusdc/src/lib.rs

fn get_profit_split(profit: u64, leverage: u64, fixed_fee_split: u64, mfee_percent: u64) -> Result<(u64,u64,u64)> {
    msg!("fixed_fee_split: {}", fixed_fee_split);
    msg!("leverage: {}", leverage);
    let fixed_split_leverage = fixed_fee_split * leverage;
    msg!("Fixed split leverage: {}", fixed_split_leverage);
    let fixed_split_adjusted = fixed_fee_split * 10;
    msg!("Fixed split adjusted: {}", fixed_split_adjusted);

    let split = fixed_split_leverage.checked_add(fixed_split_adjusted).ok_or(ErrorCode::ReceivedAmountOperationError)?;
    msg!("Split: {}", split);
    let to_water = profit.checked_mul(split).ok_or(ErrorCode::ReceivedAmountOperationError)?
                        .checked_div(10000).ok_or(ErrorCode::ReceivedAmountOperationError)?;
    msg!("To water: {}", to_water);
    let m_fee = profit.checked_mul(mfee_percent).ok_or(ErrorCode::ReceivedAmountOperationError)?
                    .checked_div(10000).ok_or(ErrorCode::ReceivedAmountOperationError)?;
    msg!("M fee: {}", m_fee);
    let to_user = profit.checked_sub(to_water.checked_add(m_fee).ok_or(ErrorCode::ReceivedAmountOperationError)?).ok_or(ErrorCode::ReceivedAmountOperationError)?;
    msg!("To user: {}", to_user);
    
    Ok((to_water, m_fee, to_user))
}

BVSS
Recommendation

To address this issue, it is recommended to cap the fees to ensure they do not exceed a maximum percentage of the profit.

Remediation

SOLVED: The Vaultka team solved the issue by limiting the maximal percentage of total fees and thus ensuring that the fees will always be less than 100 percent of the profit. This guarantees that a user always receives part of the profit and that no arithmetic issues can occur.

Remediation Hash

7.8 INABILITY TO CLOSE UNNEEDED ACCOUNTS

// Informational

Description

The program lacks functionality to close unnecessary PositionInfo and UserInfo accounts, resulting in rent lamports being permanently locked.

BVSS
Recommendation

To address this issue, it is recommended to close the unneeded accounts and sent the rent back to the initializer.

Remediation

ACKNOWLEDGED: The client acknowledged the finding.

7.9 FORMAL ISSUES AND RECOMMENDATIONS

// Informational

Description

The project is written in Rust and utilizes the Anchor framework to improve code clarity and security. While the program generally makes effective use of Rust and Anchor, some areas could be enhanced to better align with common Solana development conventions and best software engineering practices. Additionally, numerous formal issues, such as incorrect naming, unused variables, and inaccurate comments, need to be addressed.

BVSS
Recommendation

To address this issue, it is recommended to implement following actions:

  • Do not redefine accounts and instruction data structs (such as lending::cpi::accounts::Repay) for CPI calls.

  • Avoid recalculating PDAs and corresponding bumps when not necessary.

    • Use ctx.bumps.program_authority instead of Pubkey::find_program_address(...) .

  • To reload accounts after CPI completion, use the reload() method.

    • For example: ctx.accounts.jlp_token_account.reload()?;

  • Avoid using msg!() macro if not necessary.

  • Avoid truncating downcast.

    // instead of truncating downcast
    let variable_u64 = variable_u128 as u64;
    // prefer using 
    let variable_u64 = u64::try_from(variable_u128).map_err(...)?;
  • Add programs as last accounts in the context struct.

  • Preferably perform the access control checks in the context struct to separate it from the business logic.

    // For example in set_vault_keeper, instead of 
    if ctx.accounts.user.key() != vault_data.owner {}
    // use rather the has_one attribute
    #[account(mut, has_one = owner, seeds = [b"LENDING".as_ref()], bump)]
    pub lending_account: Account<'info, Lending>,    
    #[account(mut)]    
    pub owner: Signer<'info>, // rename user to owner
    
    // Instead of passing unchecked AccountInfo
    pub lending_program: AccountInfo<'info>,
    // prefer using dedicated Anchor types such as
    pub lending_program: Program<‘info, LendingProgram>
  • Avoid separate variables declaration and initialization.

  • Define seeds as constants and reuse them across accounts.

  • Modularize the program and split it in multiple files.

  • Do not use magic numbers.

  • Avoid passing unnecessary (unused) accounts to instructions.

    • The System program in SetVaultKeeper, SetFeeParams and SetMaxUtilRate instructions is not needed.

    • The strategy account in ManualPriceUpdate, SetSlippageControl, SetJLPPriceSlippage instructions is not needed.

  • Remove the invalid #[instruction(to_whitelist: Pubkey)] macro annotation from SetVaultKeeper struct.

  • Use more descriptive names for accounts.

    • For example the account user in SetFeeParams, SetWhitelisted, SetMaxUtilRate, SetVaultKeeper or DisableWhitelisted instruction should rather be owner .

  • Return correct errors:

    • After checked_div operations, only division by zero error can happen and not overflow or underflow.

    • Review all errors returned.

    • waterusdc/src/lib.rs

      pub fn repay(ctx: Context<Repay>, repay_amount: u64, debt_value: u64) -> Result<()> {
              let ra:u128;
              ra = repay_amount as u128;
              let dv:u128;
              dv = debt_value as u128;
      
              if ra <= 0 {
                  return err!(ErrorCode::InvalidRepayAmount);
              }
      
              if dv <= 0 {
                  return err!(ErrorCode::InvalidRepayAmount); // FIX incorrect error
              }
      
              let whitelisted = &ctx.accounts.whitelisted_borrower;
              if !whitelisted.whitelisted {
                  return err!(ErrorCode::InvalidBorrowAmount); // FIX incorrect error
              }
      // ...
      }
  • Rename incorrect variable names:

    • waterusdc/src/lib.rs

      pub fn set_strategy_params(
              ctx: Context<SetStrategyParams>,
              dtv_limit: u64,
              keeper: Pubkey,
              keeper_ata: Pubkey,
              leverage_limit: u64,
              fixed_fee_split: u64,
              mfee_percent: u64,
              sol_jlp_feed: [u8; 32], // FIX incorrect name 
              jlp_sol_feed: [u8; 32], // FIX incorrect name 
              jlp_mint: Pubkey,
              keeper_fees: u64,
              usdc_mint: Pubkey,
              maturity_time: i64) -> ProgramResult {...)
  • Use cargo clippy and resolve all warnings.


Remediation

ACKNOWLEDGED: The client acknowledged the 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-2024-0344

curve25519-dalek

Timing variability in curve25519-dalek's Scalar29::sub/Scalar52::sub

RUSTSEC-2022-0093

ed25519-dalek

Double Public Key Signing Function Oracle Attack on ed25519-dalek

Halborn strongly recommends conducting a follow-up assessment of the project either within six months or immediately following any material changes to the codebase, whichever comes first. This approach is crucial for maintaining the project’s integrity and addressing potential vulnerabilities introduced by code modifications.

© Halborn 2024. All rights reserved.