NfNode Rewards Program - Wayru


Prepared by:

Halborn Logo

HALBORN

Last Updated 05/03/2025

Date of Engagement: January 14th, 2025 - February 14th, 2025

Summary

100% of all REPORTED Findings have been addressed

All findings

7

Critical

0

High

0

Medium

2

Low

3

Informational

2


1. Introduction

Wayru engaged Halborn to conduct a security assessment on their NfNode Rewards Solana program beginning on January 14th, 2025, and ending on January 16th, 2025. The security assessment was scoped to the Solana Program provided in rewards-system-program GitHub repository. Commit hashes and further details can be found in the Scope section of this report.


The NfNode Rewards Program enables users to claim rewards based on their NfNode operation and ownership. It leverages the Anchor framework to streamline the development of Solana programs. It allows administrators to establish a reward system, fund it with tokens, and manage the reward claiming process. Users, who are NfNode owners or operators, can claim rewards once per day. Each claim requires a partial signature from an administrator, enhancing security. The user claiming the reward is responsible for the transaction fees.


The Wayru team provided an updated version of the NfNode Rewards programa which introduces a new feature that requires users to make a deposit when creating a new NfNode entry. Depending on the NfNode type, the deposit is required at the time of entry creation. For BYOD and WayruHotspot types, the deposit must be made along with the creation of the NfNode entry. For the DON type, no deposit is needed for the owner to claim rewards; however, the deposit is required for the manufacturer or host to be able to claim rewards. Additionally, users must wait a 30-day cooldown period before they can withdraw the deposited amount.


2. Assessment Summary

Halborn was provided 3 days for the engagement and assigned one full-time security engineer to review the security of the Solana Programs in scope. The engineer is a blockchain and smart contract security expert with advanced smart contract hacking skills, and deep knowledge of multiple blockchain protocols. This program allows administrators to establish a reward system, fund it with tokens, and manage the reward claiming process. Users, who are NfNode owners or operators, can claim rewards once per day.

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 improvements to reduce the likelihood and impact of risks, which were completely addressed by the Wayru team:

    • Implement a validation to ensure that the Initialize instruction can only be executed by a known and trusted address, such as the program upgrade authority.

    • Add a "owner" field in nfnode_entry to hold the user's address who initializes. Add a check in in update_nfnode and owner_claim_rewards to ensure the user who is signing is the owner of the provided nfnode_entry. Also, add a validation in init_nfnode, update_nfnode and owner_claim_rewardsto ensure the user who is signing is the authority of the user_nft_token_account provided.

    • Implementing a two-step authority transfer functionality and add a validation to ensure the new authority candidate is not the current admin.

    • Add a check to ensure the reward amount and fund amount is strictly greater than zero.

    • Add validations to Verify that the mint's total supply is exactly 1 and its decimal is 0.


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 (`solana-test-framework`)

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:
EXPLOITABILITY 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: rewards-system-program
(b) Assessed Commit ID: 9dfab91
(c) Items in scope:
  • programs/nfnode-rewards/src/errors.rs
  • programs/nfnode-rewards/src/lib.rs
  • programs/nfnode-rewards/src/state.rs
↓ Expand ↓
Out-of-Scope: Third party dependencies and economic attacks.
Files and Repository
(a) Repository: rewards-system-program
(b) Assessed Commit ID: 3b593b0
(c) Items in scope:
  • programs/nfnode-rewards/src
  • programs/nfnode-rewards/src/instructions
  • programs/nfnode-rewards/src/instructions/deposit_tokens.rs
↓ Expand ↓
Out-of-Scope: third party dependencies and economic attacks.
Out-of-Scope: New features/implementations after the remediation commit IDs.

6. Assessment Summary & Findings Overview

Critical

0

High

0

Medium

2

Low

3

Informational

2

Security analysisRisk levelRemediation Date
Program initializer can be front-runMediumSolved - 01/20/2025
Multiple vulnerabilities in reward claiming and NfNode initialization and update processMediumSolved - 01/21/2025
New admin check missingLowSolved - 01/21/2025
Zero amount check missingLowSolved - 01/21/2025
Insufficient validation of NFT mint properties in nfnode entry initializationLowSolved - 02/04/2025
Insufficient validation of mint authority at system initializationInformationalSolved - 02/17/2025
Paused status check missing in pause instructionsInformationalSolved - 01/21/2025

7. Findings & Tech Details

7.1 Program initializer can be front-run

//

Medium

Description

The current implementation of the init_system instruction lacks proper restrictions to ensure that it is signed by a known and trusted authority, such as the program's update authority. This absence of validation allows an attacker to execute the initialization process prematurely, potentially enabling an attacker to configure the program with malicious or unauthorized accounts under their control.


The lack of safeguards during the system initialization phase poses significant risks, particularly given the critical role of the admin's signature in core operations such as initializing and updating nfnode_entry entries, as well as claiming rewards. Without proper validation, this vulnerability could result in severe consequences, including enabling malicious administrators to perform unauthorized actions, as outlined in related attacks described in HAL-08, without any form of restriction.


init_system.rs:

pub fn initialize_system(ctx: Context<InitializeSystem>) -> Result<()> {
    let admin_account = &mut ctx.accounts.admin_account;
    admin_account.admin_pubkey = ctx.accounts.user.key();
    admin_account.paused = false;
    Ok(())
}

#[derive(Accounts)]
pub struct InitializeSystem<'info> {
    #[account(mut)]
    pub user: Signer<'info>,
    #[account(
        init,
        payer = user,
        space = 8 + std::mem::size_of::<AdminAccount>(),
        seeds = [b"admin_account"],
        bump
    )]
    pub admin_account: Account<'info, AdminAccount>,
    pub system_program: Program<'info, System>,
Proof of Concept


1. Call the program's initializer as low-privileged (any) user, in this case, Bob.

w1.png

BVSS
Recommendation

To address this issue, it is recommended to implement a validation to ensure that the Initialize instruction can only be executed by a known and trusted address, such as the program upgrade authority.


The following code snippet shows an example of this measure to be implemented in the init_system instruction:

    
    pub admin: Signer<'info>,
    #[account(constraint = program.programdata_address()? == Some(program_data.key()))]
    pub program: Program<'info, NfnodeRewards>,
    #[account(constraint = program_data.upgrade_authority_address == Some(user.key()))]
    pub program_data: Account<'info, ProgramData>, 
Remediation Comment

SOLVED: The Wayru team solved this issue by implementing the validation suggested, ensuring only the upgrade authority can initialize the system.

7.2 Multiple vulnerabilities in reward claiming and NfNode initialization and update process

//

Medium

Description

A user holding an NFT that was previously minted can initialize a new NfNode entry using the init_nfnode instruction. To do so, the user must provide several accounts, including the nft_mint_address, user_nft_token_account, host, manufacturer, among others. This instruction initializes an nfnode_entry, which is a Program Derived Address (PDA) composed of a fixed seed and a variable component, specifically the nft_mint_address.


The owner of this nfnode_entry, who created it, will be able to claim their corresponding rewards daily using the owner_claim_rewards instruction. Similarly, the host or manufacturer assigned by the owner during the creation of the nfnode_entry can claim their corresponding rewards daily using the other_claim_rewards instruction. Furthermore, the owner of the nfnode_entry can change the address of the host at any time.


However, the init_nfnode, update_nfnode, and owner_claim_rewards instructions do not verify that the signing user is the authority over the user_nft_token_account, nor do the latter two instructions verify that the signer is the actual owner who created the nfnode_entry.


This lack of verification results in the following vulnerabilities:


  1. Unauthorized Initialization:
    Any user who knows the user_nft_token_account address of the legitimate NFT owner can initialize an nfnode_entry by providing that user_nft_token_account and assigning its own address as the host. This would allow the attacker to subsequently claim rewards both as the host and as the owner, while the legitimate NFT owner would no longer be able to create an nfnode_entry.


  2. Unauthorized Updates:
    Any user can update the nfnode_entry of a legitimate user using the update_nfnode instruction by providing the user_nft_token_account address of the legitimate user who created the nfnode_entry and assigning its own address as the new host. This would allow the attacker to subsequently claim rewards legitimately using the other_claim_rewards instruction.


  3. Unauthorized Reward Claims:
    Any user can claim daily rewards before the legitimate owner of the nfnode_entry by providing the user_nft_token_account address of the legitimate user who created the nfnode_entry. This would prevent the rightful owner from claiming their rewards for that day.


The mentioned operations require partial approval from the admin, as their signature is necessary. However, if the admin does not have an external system outside the program to verify:

  • That the signer is legitimately the owner of the NFT in the provided user_nft_token_account.

  • That the signer is the original creator of the nfnode_entry.


The admin will lack the ability to verify which signers are legitimate, potentially resulting in the approval of transactions that introduce critical vulnerabilities. These include:

  • Reward theft from a legitimate nfnode_entry

  • Unauthorized updates to an existing nfnode_entry

  • Creation of fraudulent nfnode_entry entries using other user's nft token account. Consequently, rightful NFT owners may be prevented from initializing their entries, compromising the integrity and security of the system.


init_nfnode.rs:

pub struct InitializeNfNode<'info> {
    #[account(mut)]
    pub user_admin: Signer<'info>,
    #[account(mut)]
    pub user: Signer<'info>,
    ///CHECK: only read account
    pub host: AccountInfo<'info>,
    ///CHECK: only read account
    pub manufacturer: AccountInfo<'info>,
    ///CHECK: only read account
    pub nft_mint_address: InterfaceAccount<'info, Mint2022>,
    /// CHECK: used to check nft ownership
    pub user_nft_token_account: AccountInfo<'info>,
    #[account(
        init,
        payer = user,
        space = 8 + std::mem::size_of::<NfNodeEntry>(),
        seeds = [b"nfnode_entry", nft_mint_address.key().as_ref()],
        bump
    )]
    pub nfnode_entry: Account<'info, NfNodeEntry>,

update_nfnode.rs:

pub struct UpdateNfNode<'info> {
    #[account(mut)]
    pub user_admin: Signer<'info>,
    #[account(mut)]
    pub user: Signer<'info>,    //TODO: no parece necesario
    ///CHECK: only read account
    pub host: AccountInfo<'info>,
    ///CHECK: only read account
    pub nft_mint_address: InterfaceAccount<'info, Mint2022>,
    /// CHECK: used to check nft ownership
    pub user_nft_token_account: AccountInfo<'info>,

owner_claim_rewards.rs:

pub struct OwnerClaimRewards<'info> {
    /// CHECK:
    #[account(mut)]
    pub user_admin: Signer<'info>,
    #[account(mut)]
    pub user: Signer<'info>,
    /// CHECK:
    pub nft_mint_address: InterfaceAccount<'info, Mint2022>,
    #[account(
        init_if_needed,
        payer = user,
        space = 8 + std::mem::size_of::<RewardEntry>(),
        seeds = [b"reward_entry", user.key().as_ref(), nft_mint_address.key().as_ref()],
        bump
    )]
    pub reward_entry: Account<'info, RewardEntry>,
    #[account(
        mut,
        seeds = [b"nfnode_entry", nft_mint_address.key().as_ref()],
        bump
    )]
    pub nfnode_entry: Box<Account<'info, NfNodeEntry>>,
    pub token_mint: Account<'info, Mint>,
    /// CHECK:
    #[account(mut, seeds = [b"token_storage"], bump)]
    pub token_storage_authority: AccountInfo<'info>,
    #[account(
        mut,
        associated_token::mint = token_mint,
        associated_token::authority = token_storage_authority,
    )]
    pub token_storage_account: Box<Account<'info, TokenAccount>>,
    #[account(
        init_if_needed,
        payer = user,
        associated_token::mint = token_mint,
        associated_token::authority = user
    )]
    pub user_token_account: Box<Account<'info, TokenAccount>>,
    /// CHECK: used to check nft ownership
    pub user_nft_token_account: AccountInfo<'info>,
Proof of Concept

Steps to reproduce the attacks:


Unauthorized Initialization:

  1. Tom has a nft

  2. Bob, knowing Tom's nft token account, init a nfNode entry providing Tom's token account with the nft

  3. The Token Storage is funded

  4. Bob claims rewards since is the owner of the nfNode entry that initialized previously


w21.png

Unauthorized Updates:

  1. A legitimate user who has a nft initialize the nfNode entry setting the corresponding host and manufacturer

  2. The Token Storage is funded

  3. A malicious user (Alice) updated the nfNode entry providing the user's nft token account and setting its own address as host

  4. Alice claims rewards as legitimate host ( other_claim_rewards)


w22.png

Unauthorized Reward Claims:

  1. A legitimate user who has a nft initialize the nfNode entry setting the corresponding host and manufacturer

  2. The Token Storage is funded

  3. A malicious user (Alice) claim rewards providing the user's nft token account ( owner_claim_rewards).


w23.png
BVSS
Recommendation

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

  • Add an "owner" field in nfnode_entry to hold the user's address who initializes.

  • Add a validation in update_nfnode and owner_claim_rewards to ensure the user who is signing is the owner of the provided nfnode_entry.

  • Add a validation in init_nfnode, update_nfnode and owner_claim_rewardsto ensure the user who is signing is the authority of the user_nft_token_account provided.

Remediation Comment

SOLVED: The Wayru team solved this issue by implementing validation mechanisms in the init_nfnode, update_nfnode, and owner_claim_rewards instructions. These validations ensure that the user_nft_token_account provided corresponds to the signer's Associated Token Account (ATA) for the specified NFT mint of the nfnode. Combined with the existing validation that checks the ATA's token balance is greater than zero, these measures ensure that only the current holder of the NFT can execute these instructions, alongside the administrator's partial signature.

7.3 New admin check missing

//

Low

Description

The update_admin instruction allows the current system administrator to transfer ownership and designate a new admin. This admin role is critical within this Solana program, as its partial signature is required for operations such as initializing and updating the NF node and claiming rewards.


However, the instruction does not validate the public key provided as the new admin parameter. As a result, the following scenarios could occur:

  • Reassigning the Admin to the Current Admin
    The current admin could mistakenly assign the role to itself. While this does not disrupt the program’s functionality, it incurs an unnecessary transaction cost without achieving any effect.

  • Assigning an Invalid Address
    If an invalid or non-existent address is assigned as the new admin, it would render most subsequent instructions inoperable due to the inability to obtain the required admin signature. This effectively creates a Denial-of-Service (DoS) scenario for critical operations dependent on the admin’s authorization.

  • Assigning an Incorrect or Malicious Address
    If the admin role is mistakenly or maliciously transferred to an unintended address, such as a malicious user’s account, this entity could exploit their new administrative privileges to compromise the program’s integrity.


update_admin.rs:

pub fn update_admin(ctx: Context<UpdateAdmin>, new_admin_pubkey: Pubkey) -> Result<()> {
   
    let admin_account = &mut ctx.accounts.admin_account;
    require!(
        ctx.accounts.user.key() == admin_account.admin_pubkey,
        RewardError::UnauthorizedAdmin
    );
    admin_account.admin_pubkey = new_admin_pubkey;
    Ok(())

While this instruction can only be executed by the current admin, assigning an invalid or incorrect address results in an irreversible outcome. In scenarios where the admin role is transferred to an invalid or malicious account, critical operations requiring admin authorization would be permanently blocked or exploited.

BVSS
Recommendation

To address this issue, consider implementing a two-step authority transfer functionality. This can be achieved by adding a function that designates a new authority candidate, ensuring that this candidate is not the zero address. Authority is only transferred once the new authority accepts it by sending a signed transaction.

It is also recommended to add a validation to ensure the new authority candidate is not the current admin.

Remediation Comment

SOLVED: The Wayru team solved this issue by adding two-step authority transfer process implementation ensuring the new candidate is not the current admin or the zero address.

7.4 Zero amount check missing

//

Low

Description

The owner_claim_rewards and others_claim_rewards instructions allow the owner and host/manufacturer , respectively, to claim rewards by providing a reward amount as a parameter. The specified amount is transferred to the owner or host/manufacturer accordingly.


However, the program does not validate the provided amount in any of these instructions, leading into potential zero amount claims. The lack of validation allows users to provide a reward amount of zero when claiming rewards. If this occurs, the user calling the instruction will receive no rewards but the fields that track the last claim timestamp will be updated as if a valid claim occurred, preventing further reward claims during that day. This effectively causes the user to lose the opportunity to claim their rightful rewards for that day, as the state reflects a completed claim despite no rewards being distributed.


Although both operations require approval from the admin, as their partial signature is necessary, it is important to add validations to prevent the aforementioned scenario as a best practice.


other_claim_rewards.rs:

reward_entry.last_claimed_nonce = nonce; 
reward_entry.last_claimed_timestamp = current_timestamp;
    //verify if host or manufacturer
    if ctx.accounts.user.key() == nfnode_entry.host {
        nfnode_entry.host_last_claimed_timestamp = current_timestamp;
    } else if ctx.accounts.user.key() == nfnode_entry.manufacturer {
        nfnode_entry.manufacturer_last_claimed_timestamp = current_timestamp;
    } else {
        return Err(RewardError::UnauthorizedUser.into());
    }
    nfnode_entry.total_rewards_claimed += reward_amount;
    
    let authority_bump = ctx.bumps.token_storage_authority;
    let authority_seeds = &[&b"token_storage"[..], &[authority_bump]];
    let signer_seeds = &[&authority_seeds[..]];
    token::transfer(
        CpiContext::new_with_signer(
            ctx.accounts.token_program.to_account_info(),
            Transfer {
                from: ctx.accounts.token_storage_account.to_account_info(),
                to: ctx.accounts.user_token_account.to_account_info(),
                authority: ctx.accounts.token_storage_authority.to_account_info(),
            },
            signer_seeds
        ),
        reward_amount
    )?;

Similarly, the fund_token_storage instruction facilitates the funding of the token storage account by allowing users to provide a funding amount as a parameter. However the absence of validation for the provided funding amount allows users to perform funding operations with an amount of zero resulting in a zero-value transfer takes place, resulting in a redundant transaction with no functional impact.

lib.rs:

  pub fn fund_token_storage(ctx: Context<FundTokenStorage>, amount: u64) -> Result<()> {
        token::transfer(ctx.accounts.transfer_to_token_storage(), amount)?;
        Ok(())
    }
BVSS
Recommendation

To address this issue, it is recommended to implement validations to ensure the reward amount in owner_claim_rewards and others_claim_rewards instructions is strictly greater than zero. Similarly, validate the funding amount in the fund_token_storage instruction to prevent zero-value operations.

Remediation Comment

SOLVED: The Wayru team solved this issue by adding:

  • A check in owner_claim_rewards and others_claim_rewards instructions to ensure the provided reward_amount value is greater than 0.

  • A validation in fund_token_storage to check the provided amount is greater than the zero value.


7.5 Insufficient validation of NFT mint properties in nfnode entry initialization

//

Low

Description

A user holding a previously minted NFT can initialize a new nfnode_entry using the init_nfnode instruction. To perform this action, the user must provide several accounts, including the nft_mint_address, user_nft_token_account, and others. This instruction initializes an nfnode_entry, which is a Program Derived Address (PDA) derived from a fixed seed and a variable component, specifically the nft_mint_address.


The instruction handler verifies that the user_nft_token_account is a token account whose associated mint matches the provided nft_mint_address and that its token amount is not zero. However, these validations are insufficient to ensure that the nft_mint_address corresponds to a valid NFT. Currently, the only validation for nft_mint_address is that it represents a Mint22, allowing any mint to pass as valid. Consequently, any user_nft_token_account associated with that mint and containing tokens would be accepted.


init_nfnode.rs:

 let user_nft_token_account_info = &ctx.accounts.user_nft_token_account;

    if user_nft_token_account_info.owner != &ctx.accounts.token_program_2022.key() {
        return err!(RewardError::InvalidNftMint);
    }

    let user_nft_token_account_data = user_nft_token_account_info.try_borrow_data()?;
    let user_nft_token_account = SplToken2022Account::try_deserialize(
        &mut &user_nft_token_account_data[..]
    )?;

    if user_nft_token_account.amount == 0 {
        return err!(RewardError::InsufficientNftBalance);
    }
   
    if user_nft_token_account.mint != ctx.accounts.nft_mint_address.key() {
        return err!(RewardError::InvalidNftMint);
    }
    let nfnode_entry = &mut ctx.accounts.nfnode_entry;
    nfnode_entry.host = ctx.accounts.host.key();
pub struct InitializeNfNode<'info> {
    #[account(mut)]
    pub user_admin: Signer<'info>,
    #[account(mut)]
    pub user: Signer<'info>,
    ///CHECK: only read account
    pub host: AccountInfo<'info>,
    ///CHECK: only read account
    pub manufacturer: AccountInfo<'info>,
    ///CHECK: only read account
    pub nft_mint_address: InterfaceAccount<'info, Mint2022>,
    /// CHECK: used to check nft ownership
    pub user_nft_token_account: AccountInfo<'info>,
    #[account(
        init,
        payer = user,
        space = 8 + std::mem::size_of::<NfNodeEntry>(),
        seeds = [b"nfnode_entry", nft_mint_address.key().as_ref()],
        bump
    )]
    pub nfnode_entry: Account<'info, NfNodeEntry>,
BVSS
Recommendation

To address this issue, it is recommended to implement additional validation within the nfnode_entry initialization process to:

  • Verify that the mint's total supply is exactly 1.

  • Ensure that the mint's decimal precision is set to 0.


Note: Although the program does not perform transfers to or from the token accounts associated with these NFT mints, the fact that they are Mint22 accounts introduces the potential for unexpected extensions. Therefore, it is advisable to include a validation step that confirms the mint does not have any undesired extensions. This precaution would help mitigate unforeseen risks associated with improperly configured mints.



Remediation Comment

SOLVED: The Wayru team solved this issue by adding a validation that ensures that the total supply and decimals are those corresponding to the NFT. In addition, the AdminAccount now has a vector where your administrator can add and remove mint_authority addresses, which will correspond to the authorities that minted the allowed nft for new nfNode entries.

7.6 Insufficient validation of mint authority at system initialization

//

Informational

Description

For the system initialization, the program’s upgrade authority is required to provide several accounts, including the mint_authority, which is added to the mint_authorities vector within the AdminAccount. The maximum amount of mint authority addresses allowed in the vector is ten. These represent the mint authorities responsible for minting the NFTs that will allow their owners to create a new NfNode entry.

However, the mint_authority account provided during initialization is not validated to ensure it corresponds to a valid and correct address. While this does not pose a significant security risk—since a trusted authority oversees the initialization process and can rectify errors by removing an incorrect mint_authority and adding the correct one—it is still recommended to implement a validation check to ensure that the provided address is at least not invalid.

init_system.rs:

pub fn initialize_system(ctx: Context<InitializeSystem>) -> Result<()> {
    let admin_account = &mut ctx.accounts.admin_account;
    admin_account.admin_pubkey = ctx.accounts.user.key();
    admin_account.paused = false;
    admin_account.valid_mint = ctx.accounts.token_mint.key();
    admin_account.mint_authorities.push(ctx.accounts.mint_authority.key());
    Ok(())
}

#[derive(Accounts)]
pub struct InitializeSystem<'info> {
    #[account(mut)]
    pub user: Signer<'info>,
    #[account(
        init,
        payer = user,
        space = 8 +
        std::mem::size_of::<AdminAccount>() +
        MAX_MINT_AUTHORITIES * std::mem::size_of::<Pubkey>(),
        seeds = [b"admin_account"],
        bump
    )]
    pub admin_account: Account<'info, AdminAccount>,
    ///CHECK: only read account
    pub mint_authority: AccountInfo<'info>,
BVSS
Recommendation

Consider to add a validation to ensure the mint_authority provided is a trusted and valid address.

Remediation Comment

SOLVED: The Wayru team solved this issue by adding a check in the init_system instruction to ensure the mint_authority provided matches the upgrade authority of the program.

7.7 Paused status check missing in pause instructions

//

Informational

Description

The administrator is granted the ability to manage the program’s operational state by pausing and unpausing it. This is achieved through the pause_program and unpause_program instructions, which respectively halt or resume the claiming of rewards.


Neither the pause_program nor the unpause_program instruction handlers validate whether the current program state already matches the intended state to be set. This oversight results in the program allowings the administrator to execute unnecessary state updates, such as pausing an already-paused program or unpausing an already-active one. These redundant actions incur transaction fees without resulting in any functional changes.


lib.rs:

   pub fn pause_program(ctx: Context<UpdateAdmin>) -> Result<()> {
        let admin_account = &mut ctx.accounts.admin_account;
        require!(
            ctx.accounts.user.key() == admin_account.admin_pubkey,
            RewardError::UnauthorizedAdmin
        );

        admin_account.paused = true;
        Ok(())
    }

    pub fn unpause_program(ctx: Context<UpdateAdmin>) -> Result<()> {
        let admin_account = &mut ctx.accounts.admin_account;
        require!(
            ctx.accounts.user.key() == admin_account.admin_pubkey,
            RewardError::UnauthorizedAdmin
        );

        admin_account.paused = false;
BVSS
Recommendation

To enhance efficiency and prevent the mentioned redundant operations, it is recommended to implement the following measures:

  • Add a check in the pause_program and unpause_program instruction handlers to verify whether the current state already matches the desired state.

  • Return a clear error message when these redundant actions are attempted, informing the administrator that the state is already set to the requested value.

Remediation Comment

SOLVED: The Wayru team solved this issue by adding a check in the pause_program and unpause_program instruction handlers to ensure that the current status is not set again.

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 2025. All rights reserved.