Prepared by:
HALBORN
Last Updated 10/04/2024
Date of Engagement by: September 9th, 2024 - October 2nd, 2024
100% of all REPORTED Findings have been addressed
All findings
15
Critical
0
High
0
Medium
0
Low
6
Informational
9
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.
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:
d8e3993f47104130680f69b1f164c065a0fda29b on develop branch
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`)
EXPLOITABILIY METRIC () | METRIC VALUE | NUMERICAL VALUE |
---|---|---|
Attack Origin (AO) | Arbitrary (AO:A) Specific (AO:S) | 1 0.2 |
Attack Cost (AC) | Low (AC:L) Medium (AC:M) High (AC:H) | 1 0.67 0.33 |
Attack Complexity (AX) | Low (AX:L) Medium (AX:M) High (AX:H) | 1 0.67 0.33 |
IMPACT METRIC () | METRIC VALUE | NUMERICAL VALUE |
---|---|---|
Confidentiality (C) | None (I:N) Low (I:L) Medium (I:M) High (I:H) Critical (I:C) | 0 0.25 0.5 0.75 1 |
Integrity (I) | None (I:N) Low (I:L) Medium (I:M) High (I:H) Critical (I:C) | 0 0.25 0.5 0.75 1 |
Availability (A) | None (A:N) Low (A:L) Medium (A:M) High (A:H) Critical (A:C) | 0 0.25 0.5 0.75 1 |
Deposit (D) | None (D:N) Low (D:L) Medium (D:M) High (D:H) Critical (D:C) | 0 0.25 0.5 0.75 1 |
Yield (Y) | None (Y:N) Low (Y:L) Medium (Y:M) High (Y:H) Critical (Y:C) | 0 0.25 0.5 0.75 1 |
SEVERITY COEFFICIENT () | COEFFICIENT VALUE | NUMERICAL VALUE |
---|---|---|
Reversibility () | None (R:N) Partial (R:P) Full (R:F) | 1 0.5 0.25 |
Scope () | Changed (S:C) Unchanged (S:U) | 1.25 1 |
Severity | Score Value Range |
---|---|
Critical | 9 - 10 |
High | 7 - 8.9 |
Medium | 4.5 - 6.9 |
Low | 2 - 4.4 |
Informational | 0 - 1.9 |
Critical
0
High
0
Medium
0
Low
6
Informational
9
Security analysis | Risk level | Remediation Date |
---|---|---|
LACK OF PROPER VALIDATION IN DEPOSIT | Low | Solved - 09/26/2024 |
INCOMPLETE NEW OWNER VALIDATION IN THE TRANSFER OWNERSHIP PROCESS | Low | Risk Accepted |
POSSIBILITY TO CLOSE STARTED CREDIT WITH COMMITMENT | Low | Solved - 09/29/2024 |
LACK OF POOL NAME LENGTH VALIDATION | Low | Solved - 09/26/2024 |
LACK OF CURRENCY CODE LENGTH VALIDATION | Low | Solved - 09/26/2024 |
RECEIVABLE CAN BE APPROVED BY DIFFERENT POOL | Low | Solved - 09/26/2024 |
LACK OF EA AND POOL OWNER TREASURY VALIDATION IN POOL CREATION | Informational | Acknowledged |
POSSIBILITY OF INCONSISTENT LATE PAYMENT FEES | Informational | Acknowledged |
LACK OF VALIDATION FOR EXTENSIONS OF THE MINT | Informational | Solved |
LACK OF VALIDATION FOR THE MINIMUM BORROW AMOUNT TO EXCEED THE FEES | Informational | Acknowledged |
POTENTIAL COMPLETE PROFIT CAPTURE VIA ADMIN FEES | Informational | Not Applicable |
MONEY MOVEMENT WHEN PROTOCOL IS PAUSED IN SOME INSTRUCTIONS | Informational | Solved - 09/28/2024 |
LACK OF DELEGATE VALIDATION IN MAKE PAYMENT | Informational | Acknowledged |
PASSING SUPERFLUOUS SYSTEM PROGRAM ACCOUNT | Informational | Solved - 09/28/2024 |
INCONSISTENCIES IN DOCUMENTATION | Informational | Partially Solved - 09/23/2024 |
// Low
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.
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>>,
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.
SOLVED: The Huma team solved this issue by adding a validation to verify the depositor is actually an approved lender.
// Low
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;
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.
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.
// Low
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
);
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.
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.
// Low
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,
pub const MAX_POOL_NAME_LENGTH: usize = 50;
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.
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.
// Low
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,
}
To resolve this issue, it is recommended to verify the correct maximal length of the currency code to be 3 bytes.
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.
// Low
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>>
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.
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.
// Informational
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.
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).
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.
// Informational
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,
);
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.
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.
// Informational
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.
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.
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.
// Informational
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)
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.
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.
// Informational
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
);
Despite the low probability of the scenario described above, consider removing the "="
from the validation.
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.
// Informational
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(),
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.
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.
// Informational
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.
Consider to implement the mentioned validation as best practice to ensure robustness and prevent potential issues before the transaction reaches failure.
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
// Informational
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>,
To address this issue, it is recommended to remove all superfluous accounts.
SOLVED: The Huma team solved this issue by removing all superfluous accounts.
// Informational
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.
/// # 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)
}
It is recommended to update the documentation to remove all the inconsistencies.
PARTIALLY SOLVED: The Huma team partially solved this issue. They updated the lib.rs file with the appropriate modifications correcting the mentioned inconsistent comments.
Halborn used automated security scanners to assist with detection of well-known security issues and vulnerabilities. Among the tools used was cargo audit
, a security scanner for vulnerabilities reported to the RustSec Advisory Database. All vulnerabilities published in https://crates.io
are stored in a repository named The RustSec Advisory Database. cargo audit
is a human-readable version of the advisory database which performs a scanning on Cargo.lock. Security Detections are only in scope. All vulnerabilities shown here were already disclosed in the above report. However, to better assist the developers maintaining this code, the auditors are including the output with the dependencies tree, and this is included in the cargo audit output to better know the dependencies affected by unmaintained and vulnerable crates.
Cargo Audit Results
ID | Crate | Desccription |
---|---|---|
RUSTSEC-2022-0093 | ed25519-dalek | Double Public Key Signing Function Oracle Attack on |
RUSTSEC-2024-0344 | curve25519-dalek | Timing variability in |
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