Halborn Logo

Huma Protocol - Huma


Prepared by:

Halborn Logo

HALBORN

Last Updated 10/04/2024

Date of Engagement by: September 9th, 2024 - October 2nd, 2024

Summary

100% of all REPORTED Findings have been addressed

All findings

15

Critical

0

High

0

Medium

0

Low

6

Informational

9


1. Introduction

Huma team engaged Halborn to conduct a security assessment on their Huma Protocol Solana program beginning on September 9th, 2024, and ending on October 2nd, 2024. The security assessment was scoped to the Solana Program provided in huma-solana-programs GitHub repository. Commit hashes and further details can be found in the Scope section of this report.

Huma protocol is an on-chain lending protocol targeting business use cases. The programs are designed to be adaptive by supporting plugins for tranche policies, fee managers, and due managers.

2. Assessment Summary

Halborn was provided 3.5 weeks for the engagement and assigned two full-time security engineer/s 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. The main ones were the following:

    • Lack of proper validation in Deposit

    • Incomplete new owner validation in Transfer Ownership process

    • Possibility to close started Credit with commitment

    • Lack of pool name length validation

    • Lack of pool currency code length validation

    • Receivable can be approved by different pool


Most of the findings were addressed, and the corresponding fixes have been merged into the branches listed below. The final commits reflect the changes that solved the issues:

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 (`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: huma-solana-programs
(b) Assessed Commit ID: 7d84086
(c) Items in scope:
  • 00labs/huma-solana-programs/blob/audit/Anchor.toml
  • 00labs/huma-solana-programs/blob/audit/Cargo.lock
  • 00labs/huma-solana-programs/blob/audit/Cargo.toml
↓ Expand ↓
Out-of-Scope: 00labs/huma-solana-programs/blob/audit/yarn.lock, 00labs/huma-solana-programs/blob/audit/Anchor.toml, 00labs/huma-solana-programs/blob/audit/Cargo.lock, 00labs/huma-solana-programs/blob/audit/Cargo.toml, 00labs/huma-solana-programs/blob/audit/programs/huma/Cargo.toml, 00labs/huma-solana-programs/blob/audit/programs/huma/Xargo.toml, 00labs/huma-solana-programs/blob/audit/programs/huma/src/common/calendar.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/common/calendar/tests.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/common/constants.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/common/errors.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/common/huma_config/instructions/create_huma_config.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/common/huma_config/instructions/liquidity_asset.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/common/huma_config/instructions/mod.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/common/huma_config/instructions/pause.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/common/huma_config/instructions/update_huma_config.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/common/huma_config/mod.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/common/huma_config/states.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/common/mod.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/common/preconditions.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/common/utils.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/credit/credit.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/credit/due_manager.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/credit/due_manager/tests.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/credit/events.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/credit/instructions/approve_credit.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/credit/instructions/approve_receivable.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/credit/instructions/close_credit.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/credit/instructions/create_receivable.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/credit/instructions/declare_payment.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/credit/instructions/drawdown.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/credit/instructions/make_payment.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/credit/instructions/make_principal_payment.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/credit/instructions/manage_credit.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/credit/instructions/manage_credit_config.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/credit/instructions/mod.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/credit/instructions/refresh_credit.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/credit/instructions/start_committed_credit.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/credit/instructions/submit_receivable.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/credit/instructions/update_receivable.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/credit/mod.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/credit/states.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/credit/test_utils.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/lib.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/pool/epoch_manager.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/pool/instructions/admin_fees.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/pool/instructions/close_epoch.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/pool/instructions/close_pool.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/pool/instructions/create_pool.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/pool/instructions/create_pool_accounts.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/pool/instructions/disable_pool.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/pool/instructions/enable_pool.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/pool/instructions/mock_pnl.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/pool/instructions/mod.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/pool/instructions/pool_operator.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/pool/instructions/update_pool_config.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/pool/mod.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/pool/pnl.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/pool/pool.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/pool/states.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/pool/tranches_policy.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/pool/tranches_policy/fixed_senior_yield_tranches_policy.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/pool/tranches_policy/risk_adjusted_tranches_policy.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/tranche_vault/instructions/add_redemption_request.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/tranche_vault/instructions/cancel_redemption_request.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/tranche_vault/instructions/close_lender_accounts.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/tranche_vault/instructions/create_lender_accounts.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/tranche_vault/instructions/deposit.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/tranche_vault/instructions/disburse.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/tranche_vault/instructions/make_initial_deposit.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/tranche_vault/instructions/manage_lender.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/tranche_vault/instructions/mod.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/tranche_vault/instructions/transfer_hook.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/tranche_vault/instructions/update_to_latest_redemption_record.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/tranche_vault/instructions/withdraw_after_pool_closure.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/tranche_vault/mod.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/tranche_vault/states.rs, 00labs/huma-solana-programs/blob/audit/programs/huma/src/tranche_vault/tranche_vault.rs, 00labs/huma-solana-programs/blob/audit/programs/tranche-token-hook/Cargo.toml, 00labs/huma-solana-programs/blob/audit/programs/tranche-token-hook/src/constants.rs, 00labs/huma-solana-programs/blob/audit/programs/tranche-token-hook/src/errors.rs, 00labs/huma-solana-programs/blob/audit/programs/tranche-token-hook/src/instructions/mod.rs, 00labs/huma-solana-programs/blob/audit/programs/tranche-token-hook/src/instructions/transfer.rs, 00labs/huma-solana-programs/blob/audit/programs/tranche-token-hook/src/lib.rs
Remediation Commit ID:
Out-of-Scope: New features/implementations after the remediation commit IDs.

6. Assessment Summary & Findings Overview

Critical

0

High

0

Medium

0

Low

6

Informational

9

Security analysisRisk levelRemediation Date
LACK OF PROPER VALIDATION IN DEPOSITLowSolved - 09/26/2024
INCOMPLETE NEW OWNER VALIDATION IN THE TRANSFER OWNERSHIP PROCESSLowRisk Accepted
POSSIBILITY TO CLOSE STARTED CREDIT WITH COMMITMENTLowSolved - 09/29/2024
LACK OF POOL NAME LENGTH VALIDATIONLowSolved - 09/26/2024
LACK OF CURRENCY CODE LENGTH VALIDATIONLowSolved - 09/26/2024
RECEIVABLE CAN BE APPROVED BY DIFFERENT POOLLowSolved - 09/26/2024
LACK OF EA AND POOL OWNER TREASURY VALIDATION IN POOL CREATIONInformationalAcknowledged
POSSIBILITY OF INCONSISTENT LATE PAYMENT FEESInformationalAcknowledged
LACK OF VALIDATION FOR EXTENSIONS OF THE MINTInformationalSolved
LACK OF VALIDATION FOR THE MINIMUM BORROW AMOUNT TO EXCEED THE FEESInformationalAcknowledged
POTENTIAL COMPLETE PROFIT CAPTURE VIA ADMIN FEESInformationalNot Applicable
MONEY MOVEMENT WHEN PROTOCOL IS PAUSED IN SOME INSTRUCTIONSInformationalSolved - 09/28/2024
LACK OF DELEGATE VALIDATION IN MAKE PAYMENTInformationalAcknowledged
PASSING SUPERFLUOUS SYSTEM PROGRAM ACCOUNTInformationalSolved - 09/28/2024
INCONSISTENCIES IN DOCUMENTATIONInformationalPartially Solved - 09/23/2024

7. Findings & Tech Details

7.1 LACK OF PROPER VALIDATION IN DEPOSIT

// Low

Description

The AddApprovedLenders instruction allows the operator to add approved lenders who will be able to create their lender accounts to start depositing and trading in the pool.


The RemoveApprovedLenders instruction allows the operator to remove an approved lender who will no longer be able to deposit. However, due to a lack of validation in the Deposit instruction, approved lenders who have been removed by the operator can still be able to deposit to keep earning yield and sharing looses.

deposit.rs

pub struct Deposit<'info> {
    pub depositor: Signer<'info>,
    #[account(
        mut,
        seeds = [
            LENDER_STATE_SEED,
            tranche_mint.key().as_ref(),
            depositor.key().as_ref(),
        ],
        bump = lender_state.bump,
    )]
    pub lender_state: Box<Account<'info, LenderState>>,

BVSS
Recommendation

To address this issue, it is recommended to add a call to preconditions::only_approved_lender to verify the lender who is trying to deposit is a current-approved lender.

Remediation

SOLVED: The Huma team solved this issue by adding a validation to verify the depositor is actually an approved lender.

Remediation Hash

7.2 INCOMPLETE NEW OWNER VALIDATION IN THE TRANSFER OWNERSHIP PROCESS

// Low

Description

The ChangeOwner instruction allows the current Huma owner to transfer ownership to a new owner. However, since the new owner's address is passed as a parameter and the only validation ensures that the new and current owners do not match, this presents a potential risk. In the event of an error or typo, the ownership could be transferred to an incorrect or even invalid address, resulting in the irreversible loss of administrative control.

A similar safeguard should be applied to pool ownership transfers, which are the responsibility of the current pool owner or the Huma owner.

update_huma_config.rs

pub(crate) fn change_owner(ctx: Context<UpdateHumaConfig>, new_owner: Pubkey) -> Result<()> {
    let huma_config = ctx.accounts.huma_config.as_mut();
    if huma_config.owner != new_owner {
        huma_config.owner = new_owner;

update_pool_config.rs

pub(crate) fn change_owner(ctx: Context<UpdatePool>, new_owner: Pubkey) -> Result<()> {
    preconditions::only_pool_owner_or_huma_owner(
        ctx.accounts.huma_config.as_ref(),
        ctx.accounts.pool_config.as_ref(),
        &ctx.accounts.signer,
    )?;

    let pool_config = ctx.accounts.pool_config.as_mut();
    pool_config.pool_owner = new_owner;

BVSS
Recommendation

Although this instruction can only be invoked by the current Huma owner, thereby reducing the likelihood of this risk, it is recommended to enhance security by implementing a two-step ownership transfer process or other safe mechanism to avoid this risk. This approach would help mitigate the risk of ownership being transferred to an unintended address.

Remediation

RISK ACCEPTED: The Huma team accepted the risk of this finding due to they will implement multisig and timelock mechanisms for all instructions that update the Huma and pool configurations off chain. This ensures that any changes, including ownership transfers, are subject to review by at least two individuals before execution, with additional oversight likely provided during the timelock period.

7.3 POSSIBILITY TO CLOSE STARTED CREDIT WITH COMMITMENT

// Low

Description

The credit line with commitment requires the borrower to pay interest. If the outstanding principal balance is below this amount, the borrower will be charged interest for the committed amount. The interest starts to accrue from the credit start date.

The EA is supposed to call the StartCommitedCredit instruction once the credit start date has passed. However, the borrower can front-run the EA and close the credit without paying any interest or fees.

The reversibility of this issue is classified as partial, given that all borrowers are expected to be institutional entities bound by legal off-chain agreements. In the event of a breach, legal action can be taken to enforce the terms and recover any outstanding interest. Without such legal recourse, the severity of this issue would be rated higher.

close_credit.rs

require!(
     matches!(cr.status, CreditStatus::Approved)
       || cc.committed_amount == 0
       || cr.remaining_periods == 0,
       Error::CreditHasUnfulfilledCommitment
);

BVSS
Recommendation

To resolve this issue, it is recommended to make sure the borrower cannot close a credit line with commitment once the start date has passed without paying the corresponding interest.

Remediation

SOLVED: The Huma team mentioned that all of their borrowers are institutional entities, and they establish off-chain legal agreements with them before extending credit lines. Consequently, there is only a theoretical possibility for the borrowers to breach these contracts and close credit lines before they start. Should a breach occur, they can initiate legal proceedings to enforce the agreed-upon terms and recover any outstanding interest, hence the risk of the likelihood of the described scenario is very low. Nevertheless, the Huma team solved this issue by ensuring that an approved credit with commitment cannot be closed after the start date has passed.

Remediation Hash

7.4 LACK OF POOL NAME LENGTH VALIDATION

// Low

Description

The create_pool instruction requires several parameters, one of which is the pool name. However, there is currently no validation to ensure that the length of the pool name does not exceed the maximum allowed limit, potentially leading to input that violates expected constraints.

create_pool.rs

pub(crate) fn create_pool(
    ctx: Context<CreatePool>,
    pool_id: Pubkey,
    pool_name: String,
    pool_owner_treasury: Pubkey,
    evaluation_agent: Pubkey,
    tranches_policy_type: TranchesPolicyType,
) -> Result<()> {

    let pool_config = ctx.accounts.pool_config.as_mut();
    pool_config.huma_config = ctx.accounts.huma_config.key();
    pool_config.pool_id = pool_id;
    pool_config.bump = ctx.bumps.pool_config;
    pool_config.pool_authority_bump = ctx.bumps.pool_authority;
    pool_config.pool_owner = ctx.accounts.owner.key();
    pool_config.pool_name = pool_name.clone();

states.rs

pub struct PoolConfig {
    pub huma_config: Pubkey,
    pub pool_id: Pubkey,
    pub bump: u8,
    pub pool_authority_bump: u8,
    pub junior_mint_bump: u8,
    pub senior_mint_bump: Option<u8>,
    pub pool_owner: Pubkey,
    #[max_len(MAX_POOL_NAME_LENGTH)]
    pub pool_name: String,

constant.rs

pub const MAX_POOL_NAME_LENGTH: usize = 50;
BVSS
Recommendation

To address this issue, it is recommended to add a validation to verify the pool name provided to not exceed the MAX_POOL_NAME_LENGTH.

Remediation

SOLVED: The Huma team addressed this issue by introducing a new constant, MAX_POOL_NAME_LENGTH, and ensuring that the pool name length does not exceed this limit.

Remediation Hash

7.5 LACK OF CURRENCY CODE LENGTH VALIDATION

// Low

Description

The CreateReceivable instruction requires some values as parameters, one of them is the currency code. However this value is not validated to check if its length exceeds the max length allowed.

create_receivable.rs

receivable_info.set_inner(ReceivableInfo::new(
    ctx.bumps.receivable_info,
    args.currency_code.clone(),
    args.receivable_amount,
    timestamp,
    args.maturity_date,
    ctx.accounts.owner.key(),
));

states.rs

pub struct ReceivableInfo {
    pub bump: u8,
    #[max_len(3)]
    pub currency_code: String,
    pub receivable_amount: u128,
    pub amount_paid: u128,
    pub creation_date: u64,
    pub maturity_date: u64,
    pub creator: Pubkey,
    pub state: ReceivableState,
}
BVSS
Recommendation

To resolve this issue, it is recommended to verify the correct maximal length of the currency code to be 3 bytes.

Remediation

SOLVED: The Huma team addressed this issue by introducing a new constant, MAX_CURRENCY_CODE_LENGTH, and ensuring that the currency code length does not exceed this limit.

Remediation Hash

7.6 RECEIVABLE CAN BE APPROVED BY DIFFERENT POOL

// Low

Description

The instruction CreateReceivable allows anyone to create a receivable NFT. The instruction requires also passing the PoolConfig and PoolState accounts and verifies that the pool is not disabled. However the instructions SubmitReceivable nor ApproveReceivable do not verify that the NFT is being approved for the same pool as it was created for.

It is therefore possible to create Receivables for an arbitrary pool where the borrower has approved credit that is enabled and later submit or approve them for another pool.

create_receivable.rs

pub struct CreateReceivable<'info> {
    /// The address of the new receivable.
    #[account(mut)]
    pub asset: Signer<'info>,

    /// This will be the `authority`, `owner` and `update_authority` of the receivable,
    /// as well as the one paying for account storage.
    #[account(mut)]
    pub owner: Signer<'info>,

    /// CHECK: Read only authority.
    #[account(
        seeds = [
            HUMA_PROGRAM_AUTHORITY_SEED
        ],
        bump,
    )]
    pub huma_program_authority: UncheckedAccount<'info>,

    #[account(
        seeds = [
            HUMA_CONFIG_SEED,
            huma_config.id.as_ref(),
        ],
        bump = huma_config.bump,
    )]
    pub huma_config: Box<Account<'info, HumaConfig>>,

    #[account(
        seeds = [
            POOL_CONFIG_SEED,
            pool_config.pool_id.as_ref(),
        ],
        bump = pool_config.bump,
        has_one = huma_config @ Error::InvalidHumaConfig,
    )]
    pub pool_config: Box<Account<'info, PoolConfig>>,

    #[account(
        seeds = [
            POOL_STATE_SEED,
            pool_config.key().as_ref(),
        ],
        bump = pool_state.bump,
    )]
    pub pool_state: Box<Account<'info, PoolState>>
BVSS
Recommendation

To resolve this issue, it is recommended to either verify the correct pool also in both the SubmitReceivable and ApproveReceivable instructions, or, if the receivable is not meant to be tied to a specific pool, remove the PoolState and PoolConfig accounts from the CreateReceivable instruction.

Remediation

SOLVED: The Huma team solved this issue by dissociating receivables with the pool and Huma config and by removing the HumaConfig, PoolState and PoolConfig accounts from the CreateReceivable, DeclarePayment and UpdateReceivable instructions.

Remediation Hash

7.7 LACK OF EA AND POOL OWNER TREASURY VALIDATION IN POOL CREATION

// Informational

Description

The create_pool instruction requires several parameters, including the evaluation_agent and pool_owner_treasury addresses. However, this instruction lacks validation for these addresses, meaning that providing an invalid or incorrect address for either role could cause issues when attempting to update them later.

create_pool.rs

pub(crate) fn create_pool(
    ctx: Context<CreatePool>,
    pool_id: Pubkey,
    pool_name: String,
    pool_owner_treasury: Pubkey,
    evaluation_agent: Pubkey,
    tranches_policy_type: TranchesPolicyType,
) -> Result<()> {

    let pool_config = ctx.accounts.pool_config.as_mut();
    pool_config.huma_config = ctx.accounts.huma_config.key();
    pool_config.pool_id = pool_id;
    pool_config.bump = ctx.bumps.pool_config;
    pool_config.pool_authority_bump = ctx.bumps.pool_authority;
    pool_config.pool_owner = ctx.accounts.owner.key();
    pool_config.pool_name = pool_name.clone();
    pool_config.underlying_mint = ctx.accounts.underlying_mint.key();
    pool_config.pool_owner_treasury = pool_owner_treasury;
    pool_config.evaluation_agent = evaluation_agent;

Both the set_pool_owner_treasury and set_evaludation_agent instructions require the corresponding underlying token account of the old account to be changed. In the case of an invalid address, it will not be possible to change it due to this requirement.

updtae_pool_config.rs

  #[account(
        mut,
        associated_token::mint = underlying_mint,
        associated_token::authority = pool_authority,
        associated_token::token_program = token_program
    )]
    pub pool_underlying_token: Box<InterfaceAccount<'info, TokenAccount>>,

    #[account(
        mut,
        associated_token::mint = underlying_mint,
        associated_token::authority = pool_config.pool_owner_treasury,
        associated_token::token_program = token_program
    )]
    pub pool_owner_treasury_underlying_token: Box<InterfaceAccount<'info, TokenAccount>>,

Despite the low probability of such a scenario due to the fact that it is the pool creator's responsibility to assign such values, it is advisable to take this situation into account as good practice.

BVSS
Recommendation

Consider the following options as best practices:

  • add a check at pool creation to validate that these accounts are not invalid.

  • set as optional the corresponding token account required in the instructions mentioned for this kind of change, so that if by mistake an incorrect or invalid address is assigned at the time of creation, it is possible to change it directly without its token account (as long as the pool has not been enabled and there are no accumulated fees).

Remediation

ACKNOWLEDGED: The Huma team acknowledged this finding as the responsibility ultimately lies with the pool owner, who is expected to operate under a multisig structure. Additionally, it is intentional to leave the decision of whether to provide the pool owner treasury as multisig at the discretion of the pool owner. Therefore, it has been decided that the current implementation will remain unchanged.

7.8 POSSIBILITY OF INCONSISTENT LATE PAYMENT FEES

// Informational

Description

The setPoolSettings instruction allows either the pool owner or the Huma protocol owner to modify various pool parameters, including the late payment grace period. However, this could lead to inconsistent fees charged to different borrowers.

For example, let's consider two borrowers, borrowerA and borrowerB, each with the same approved credit lines. If both fail to pay off their debts within the grace period, the protocol imposes additional late payment fees. If borrowerA's credit is refreshed (e.g., by using the refreshCredit instruction, which anyone can trigger) after the grace period ends and the grace period is then extended, borrowerA would still be charged the late fee, while borrowerB would avoid it.

The severity of this finding was downgraded due to several factors that lower the likelihood of inconsistent fees. Currently, each pool is expected to have only one borrower, and all credit accounts are refreshed every 10 minutes. Most importantly, any incorrect late fees can be waived using the WaveLateFee instruction.

refresh_credit.rs

let (new_cr, new_dd) = due_manager::get_due_info(
      pool_settings.late_payment_grace_period_days,
      &pool_settings.pay_period_duration,
      fee_structure.late_fee_bps,
      &cc,
      &cr,
      &dd,
      timestamp,
);

due_manager.rs

(new_dd.late_fee_updated_date, new_dd.late_fee) = refresh_late_fee(
    cr,
    dd,
    pay_period_duration,
    late_fee_bps,
    cc.committed_amount,
    timestamp,
);
BVSS
Recommendation

To address this issue, when the late payment grace period is extended, it is recommended to automatically wave late payment fee that accrued between the original late payment grace period due date and the last late payment fee update.

Remediation

ACKNOWLEDGED: The Huma team stated that all of their borrowers are institutional borrowers, and there is currently only one borrower per pool. Even if there were to be multiple borrowers per pool, the consequence can be reversed by calling the waive_late_fee instruction to waive the incorrectly charged late fees. Hence, the Huma team acknowledged this finding and will maintain the current implementation.

7.9 LACK OF VALIDATION FOR EXTENSIONS OF THE MINT

// Informational

Description

The Huma protocol allows the Huma owner to add liquidity assets by providing the corresponding mint account, which can then be used as the underlying asset for creating pools.

However, the add_liquidity_asset instruction handler does not validate the mint extensions provided for the creation of the liquidity asset, which will be used within the pool. Certain mint tokens have associated extensions that may introduce security risks.

For example, if the mint token contains the TransferFeeConfig extension, a fee is automatically applied during each transfer, reducing the final amount received in the destination token account. This discrepancy could lead to data inconsistencies, as records such as the deposit_record would reflect the original amount sent, differing from the amount actually deposited into the pool’s underlying token account. Consequently, careful attention must be paid to the mint tokens used for liquidity assets.


add_liquidity_asset.rs

pub(crate) fn add_liquidity_asset(ctx: Context<AddLiquidityAsset>) -> Result<()> {
    let token_decimals = ctx.accounts.mint.decimals;
    // Make sure the number of decimals is neither too high or too low to prevent precision loss
    // and overflow.
    require!(
        token_decimals >= MIN_TOKEN_DECIMALS && token_decimals <= MAX_TOKEN_DECIMALS,
        Error::InvalidNumberOfDecimalsForLiquidityAsset
    );

    ctx.accounts.liquidity_asset.bump = ctx.bumps.liquidity_asset;
pub struct AddLiquidityAsset<'info> {
    #[account(mut)]
    pub owner: Signer<'info>,

    #[account(
        seeds = [
            HUMA_CONFIG_SEED,
            huma_config.id.as_ref(),
        ],
        bump = huma_config.bump,
        has_one = owner @ Error::HumaOwnerRequired,
    )]
    pub huma_config: Box<Account<'info, HumaConfig>>,

    pub mint: Box<InterfaceAccount<'info, Mint>>,

    #[account(
        init,
        seeds = [
            LIQUIDITY_ASSET_SEED,
            huma_config.key().as_ref(),
            mint.key().as_ref(),
        ],
        bump,
        payer = owner,
        space = 8 + LiquidityAsset::INIT_SPACE,
    )]
    pub liquidity_asset: Box<Account<'info, LiquidityAsset>>,

The severity of this issue depends on the reliability of the Huma owner and their understanding of the extensions and attributes of the mint tokens they provide. Given that the Huma owner will operate under a multisig configuration, any inadvertent addition of a problematic mint can be promptly addressed by removing the token. While this reduces the criticality of the issue, the potential impact remains significant. If not corrected promptly, pools using such tokens could begin operations, leading to irreversible issues until the pool or protocol is paused or closed.

BVSS
Recommendation

A list of extensions has been provided to developers to check off chain any mint that is provided to add a liquidity asset in order to avoid this issue. However, it is still recommended to implement a validation to only allow mint accounts whose extensions are the expected ones to be supported.

Remediation

SOLVED: The Huma team solved this issue by modifying the add_liquidity_asset instruction handler. It now includes validation to ensure that the provided mint token does not contain unsupported or restricted extensions for the program, specifically NonTransferable and ConfidentialTransferMint extensions, as well as TransferFeeConfig as long as its fee basis point value is not zero, thereby ensuring data consistency, preventing participation from being blocked, and enhancing program security against risks associated with SPL Token 2022 extensions.

Additionally, they will carefully inspect all token extensions before adding them as liquidity assets. With the Huma owner operating under a multisig setup, multiple reviewers will verify the token extensions to prevent any unexpected behaviors, further mitigating risk.

Remediation Hash

7.10 LACK OF VALIDATION FOR THE MINIMUM BORROW AMOUNT TO EXCEED THE FEES

// Informational

Description

The distribute_borrow_amount() function, called during the drawdown process, calculates the distribution of fees from the provided borrow amount. These fees are allocated to:

  • Protocol fees

  • Pool owner fees

  • Evaluation agent (EA) fees

Additionally, the function computes the net amount that the borrower will receive. However, this function only checks that the total calculated fees are not greater than the borrow amount, allowing the fees to be equal to the borrow amount. This scenario would result in the entire borrowed amount being used to cover fees, leaving the borrower with zero tokens.


due_manager.rs

pub fn distribute_borrow_amount(
    amount: u128,
    front_loading_fee_flat: u128,
    front_loading_fee_bps: u16,
) -> Result<(u128, u128), Error> {
    let platform_fees = front_loading_fee_flat
        + amount * (front_loading_fee_bps as u128) / (HUNDRED_PERCENT_BPS as u128);
    if amount < platform_fees {
        Err(Error::BorrowAmountLessThanPlatformFees)

BVSS
Recommendation

Although the probability of this risk is low, it is recommended to implement a minimum drawdown amount for borrowers to ensure that the entire borrowed amount is not consumed solely by fees after its calculation.

Remediation

ACKNOWLEDGED: The Huma team thinks that the implementation could go either way. First, it is unlikely that a borrower would select an amount that exactly matches the platform fees, as there is no advantage for them to do so. Second, simply removing the equals sign has minimal impact, since the difference between a borrower receiving $0 and $0.00001 is negligible. Additionally, establishing a minimum threshold for the net borrow amount is challenging because it’s unclear what threshold is “correct”. Therefore, they will keep the current implementation, along with other similar boundary checks, as they are.


7.11 POTENTIAL COMPLETE PROFIT CAPTURE VIA ADMIN FEES

// Informational

Description

The CreateHumaConfig instruction allows for the creation of the Huma configuration account by providing a set of parameters, one of which is protocol_fee_bps, later used to calculate the protocol fees. This values can be modified at any time calling the UpdateHumaConfig instruction by the huma owner.

Additionally, during pool creation, the default values used to calculate the fees for the evaluation agent and pool owner treasury are set, but these can be modified at any time. These fees, collectively referred to as admin fees, consist of:

  • protocol_fee_bps

  • reward_rate_bps_for_ea

  • reward_rate_bps_for_pool_owner

While there is validation to ensure that the total of these fees does not exceed the maximum allowable range, it still permits a configuration where the total fees sum to 100%. This could result in the entire profit being allocated as admin fees.

create_huma_config.rs

    require!(
        protocol_fee_bps <= PROTOCOL_FEE_UPPER_BOUND,
        Error::ProtocolFeeHigherThanUpperLimit
    );

update_pool_config.rs

require!(
        admin_rnr.reward_rate_bps_for_pool_owner <= HUNDRED_PERCENT_BPS as u16
            && admin_rnr.liquidity_rate_bps_for_pool_owner <= HUNDRED_PERCENT_BPS as u16
            && admin_rnr.reward_rate_bps_for_ea <= HUNDRED_PERCENT_BPS as u16
            && admin_rnr.liquidity_rate_bps_for_ea <= HUNDRED_PERCENT_BPS as u16,
        Error::InvalidBasisPointHigherThan10000
    );
    // Since we split the profit between the pool owner and EA, their combined reward rate
    // cannot exceed 100%.
    require!(
        admin_rnr.reward_rate_bps_for_pool_owner + admin_rnr.reward_rate_bps_for_ea
            <= HUNDRED_PERCENT_BPS as u16,
        Error::AdminRewardRateTooHigh
    );

BVSS
Recommendation

Despite the low probability of the scenario described above, consider removing the "=" from the validation.

Remediation

NOT APPLICABLE: This issue is not applicable, as there are scenarios where capturing the entire profit through administrative fees is strategically intentional. While such cases will be rare, they are not entirely improbable. Therefore, the Huma team have decided to retain the current implementation as it stands.

7.12 MONEY MOVEMENT WHEN PROTOCOL IS PAUSED IN SOME INSTRUCTIONS

// Informational

Description

The Huma protocol allows the addition of pausers, who are granted the authority to pause protocol operations. Only the Huma owner retains the ability to unpause the protocol. While the protocol is paused, it is expected that all financial transactions should be halted. However, the make_initial_deposit and withdraw_after_pool_closure instructions currently lack a validation check to ensure that the protocol is not in a paused state, potentially allowing unauthorized fund movements during this period.

make_initial_deposit.rs

pub(crate) fn make_initial_deposit(ctx: Context<MakeInitialDeposit>, assets: u64) -> Result<u64> {
    
    require!(
        ctx.accounts.depositor.key() == ctx.accounts.pool_config.pool_owner_treasury
            || ctx.accounts.depositor.key() == ctx.accounts.pool_config.evaluation_agent,
        Error::AuthorizedInitialDepositorRequired
    );

    tranche_vault::deposit(

withdraw_after_pool_closure.rs

pub(crate) fn withdraw_after_pool_closure(ctx: Context<WithdrawAfterPoolClosure>) -> Result<()> {
    preconditions::require_pool_closed(ctx.accounts.pool_state.as_ref())?;
    
    let tranche_mint_key = ctx.accounts.tranche_mint.key();
    preconditions::only_valid_tranche_mint(
        ctx.accounts.pool_config.as_ref(),
        ctx.accounts.pool_state.as_ref(),
        ctx.program_id,
        &tranche_mint_key,
    )?;

    // First, disburse all the funds from the lender's previously processed redemption requests.
    let mut withdrawable = tranche_vault::disburse(
        ctx.accounts.pool_state.as_mut(),

BVSS
Recommendation

Although it does not entail a risk for the lenders or for the liquidity of the pool, it is advisable to consider adding a validation to verify the protocol is not paused.

Remediation

SOLVED: The Huma team addressed this issue, adding a validation in make_initial_deposit and withdraw_after_pool_closure instruction handlers to verify the Huma protocol is not paused.

Remediation Hash

7.13 LACK OF DELEGATE VALIDATION IN MAKE PAYMENT

// Informational

Description

The MakePayment instruction facilitates a payment toward the credit line. It can be initiated by either the borrower or the Sentinel Service account, provided the borrower has granted the necessary allowance. However, the current implementation lacks validation to confirm that the sentinel is indeed the delegate for the borrower's token account from which the transfer is being made. Additionally, there is no check to ensure that the transfer amount does not exceed the delegate's approved allowance.

make_payment.rs

pub(crate) fn make_payment(ctx: Context<MakePayment>, amount: u128) -> Result<MakePaymentResult> {
    let huma_config = ctx.accounts.huma_config.as_ref();
    preconditions::require_protocol_and_pool_on(huma_config, ctx.accounts.pool_state.as_ref())?;
    only_borrower_or_sentinel(&ctx)?;
fn only_borrower_or_sentinel(ctx: &Context<MakePayment>) -> Result<()> {
    let signer_key = ctx.accounts.signer.key();
    require!(
        ctx.accounts.huma_config.sentinel == signer_key
            || ctx.accounts.credit_state.borrower == signer_key,
        Error::BorrowerOrSentinelRequired

While the instruction will fail if the current sentinel is not the valid delegate or the delegate amount is lower, it is considered best practice to explicitly implement such validation, along with proper error handling.

BVSS
Recommendation

Consider to implement the mentioned validation as best practice to ensure robustness and prevent potential issues before the transaction reaches failure.

Remediation

ACKNOWLEDGED: The Huma team acknowledged this finding since they consider the default errors thrown in both situations (not a delegate and not enough allowance) seem clear enough to show what has happened, so they will leave the current implementation as-is

7.14 PASSING SUPERFLUOUS SYSTEM PROGRAM ACCOUNT

// Informational

Description

The instructions ManageCredit, ManageCreditConfig, TriggerDefault, RefreshCredit, StartCommittedCredit required passing also the SystemProgram account. However this account is not used in the program and is therefore superfluous.

Passing the SystemProgram account only increases transaction costs.

manage_credit.rs

pub system_program: Program<'info, System>,
pub system_program: Program<'info, System>,

manage_credit_config.rs

pub system_program: Program<'info, System>,

refresh_credit.rs

pub system_program: Program<'info, System>,

start_committed_credit.rs

pub system_program: Program<'info, System>,
Score
Impact:
Likelihood:
Recommendation

To address this issue, it is recommended to remove all superfluous accounts.

Remediation

SOLVED: The Huma team solved this issue by removing all superfluous accounts.

Remediation Hash

7.15 INCONSISTENCIES IN DOCUMENTATION

// Informational

Description

Some inconsistencies have been found in Huma Protocol Spec for Solana document:

  • In the Introduction section: "Institutional investor participation. The most critical thing for institutional investors is principal safety. This makes tranche support essential so that intentional investors can choose to participate in senior tranches only." However, investor can choose to participate in a senior tranche only, but if the pool has the senior tranche (in addition to the juniot tranche, since there is no pool with only a senior tranche)

  • In the Pool-level User Roles , section (3.1.2 ) "Pool Owners: Pool owners are a list of addresses that are approved by the Protocol Owner to create and manage pools.". However, a pool can be created by anyone.

  • In the Credit Approval , section (5.1.2) : " For example, assuming the approved credit is 1000, the borrower has borrowed 500, and the borrower pays back 400. If it is not revolving, the remaining credit is only 500." However, this is Inaccurate/confusing revolving description. The non-revolving credit borrowers can drawdown only once and not multiple times which is not explicitly stated in the docs.

  • In the Credit Approval , section (5.1.2) : "The credit limit cannot exceed 4 billion of the coin (stablecoin) supported by the pool." This requirement is not relevant and should be removed.

  • In the Redemption Request and Cancellation, section (4.2.1)"only requests that meet lockup period requirements will be accepted. Redemption requests can be canceled before the epoch starts to process the requests at no cost." This can be not accurate and can be confusing, as a non processed redemption request that has been rolled up to the next epoch can also be cancelled at no cost.

Other inconsistencies have been found in lib.rs file in the program:

In add_pool_operator the comments mention the huma owner can call this instruction. However, only the pool owner can add operators.

lib.rs

 /// # Access Control
    /// Only the pool owner and the Huma owner can call this instruction.
    pub fn add_pool_operator(ctx: Context<AddPoolOperator>, operator: Pubkey) -> Result<()> {
        pool::add_pool_operator(ctx, operator)
    }

In remove_pool_operator the comments mention the huma owner can call this instruction. However, only the pool owner can add operators.

 /// # Access Control
    /// Only the pool owner and the Huma owner can call this instruction.
    pub fn remove_pool_operator(ctx: Context<RemovePoolOperator>, operator: Pubkey) -> Result<()> {
        pool::remove_pool_operator(ctx, operator)

In start_committed_credit the comments the mention the pool owner and sentinel can call this instruction. However, it is the Evaluation Agent and the sentinel who can call this instruction.

/// # Access Control
    /// Only the pool owner and the Sentinel Service account can call this instruction.
    pub fn start_committed_credit(ctx: Context<StartCommittedCredit>) -> Result<()> {
        credit::start_committed_credit(ctx)
    }
Score
Impact:
Likelihood:
Recommendation

It is recommended to update the documentation to remove all the inconsistencies.

Remediation

PARTIALLY SOLVED: The Huma team partially solved this issue. They updated the lib.rs file with the appropriate modifications correcting the mentioned inconsistent comments.

Remediation Hash

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

Desccription

RUSTSEC-2022-0093

ed25519-dalek

Double Public Key Signing Function Oracle Attack on ed255109-dalek

RUSTSEC-2024-0344

curve25519-dalek

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


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.