Friendly Casper Token Minter - Casper Association


Prepared by:

Halborn Logo

HALBORN

Last Updated 08/13/2025

Date of Engagement: July 4th, 2025 - August 8th, 2025

Summary

95% of all REPORTED Findings have been addressed

All findings

20

Critical

0

High

2

Medium

0

Low

5

Informational

13


1. Introduction

Casper Association engaged Halborn to conduct a security assessment of the DAO contracts, beginning on July 4th, 2025 and ending on August 8th, 2025. This security assessment was scoped to the smart contracts in the csprfun-core contracts GitHub repository.


The engagement involved a detailed, line-by-line security review of all smart contracts within the Friendly Casper Token Minter ecosystem. This included analysis of the contract code, entry point implementations, bonding curve mechanics, DEX integration, and related administrative controls.

2. Assessment Summary

Halborn's team of blockchain security specialists conducted a rigorous smart contract audit on the Friendly Casper Token Minter ecosystem. The review involved a cross-functional team of experts working over a 4 week period to uncover deeply embedded logic flaws, economic design risks, and practical implementation bugs. The primary goal was to stress-test the security posture for token issuance and trading.

The overall architecture demonstrates robust on-chain controls and correct use of Casper primitives. Most critical business logic passed all functional test cases, supporting safe minting, trading, and graduation to DEX liquidity.

However, the audit identified important areas for improvement, which have been partially addressed:

    • Insufficient input validation. Some functions accept parameters (like tax or fee rates) that can break economic incentives or degrade product safety.

    • Unexposed getter/setter functions and excessive token approvals—potentially reducing transparency and exposing contracts to privilege escalation or attack in edge cases.

    • Lack of event emissions, inconsistent error handling, and documentation gaps—reducing upgrade transparency and maintainability.

    • Numerous minor code hygiene issues: debug code, commented/dead code, naming mismatches.



3. Test Approach and Methodology

Halborn employs a combined approach of manual code review and automated security testing to ensure a balanced assessment of efficiency, thoroughness, and practicality within the scope of the smart contract review. Manual testing is essential for uncovering logical flaws, process weaknesses, and implementation issues, while automated techniques expand coverage and rapidly identify security best practice violations. The following phases and tools were utilized throughout the assessment:

    • Research into the architecture, purpose, and usage of the platform.

    • Manual code review and walkthrough.

    • Manual assessment of critical Rust variables and functions to evaluate their use and safety, focusing on identifying potential arithmeticrelated vulnerabilities.

    • Verification of cross-contract call controls.

    • Review of architecture-related logical controls.

    • Scanning Rust files for vulnerabilities using cargo audit)

    • Analysis and review of unit tests and integration tests.

    • Deployment to testnet via casper-client.


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

REPOSITORY
(a) Repository: csprfun-core
(b) Assessed Commit ID: 0dde699
(c) Items in scope:
  • contract/src/cep18/lib.rs
  • contract/src/main.rs
  • contract/src/utils.rs
↓ Expand ↓
Out-of-Scope: Third party dependencies and economic attacks.
Remediation Commit ID:
Out-of-Scope: New features/implementations after the remediation commit IDs.

6. Assessment Summary & Findings Overview

Critical

0

High

2

Medium

0

Low

5

Informational

13

Security analysisRisk levelRemediation Date
Missing Contract Address Update Functions Break Protocol When External Contracts UpgradeHighSolved - 07/16/2025
Incorrect Tax Calculation in Bonding Curve Leads to Reduced Output Amount, Protocol Fee Loss and Reserve ImbalancesHighSolved - 07/16/2025
Malicious Tax Configuration Enables DEX Pool Drainage and Blocks WCSPR ReturnsLowRisk Accepted - 07/16/2025
Missing Getter Functions Lead to Blind TradingLowSolved - 07/16/2025
Excessive Token Approvals in DEX Liquidity AdditionLowPartially Solved - 07/16/2025
Missing Getter Function for Token MetadataLowSolved - 07/16/2025
Missing Upper Limit Validation for Protocol Fee ConfigurationLowNot Solved
Missing Zero Amount Validation in Trade FunctionInformationalAcknowledged
Lacking Event Emissions for Critical Parameter ChangesInformationalAcknowledged
Missing Input Validation For Graduation ParametersInformationalAcknowledged
Mismatch Between EntryPoint Type Signatures and Their ImplementationsInformationalSolved - 07/17/2025
Unused Contract Installation FunctionInformationalAcknowledged
Unnecessary Runtime Arguments in CEP18 InitializationInformationalAcknowledged
Unnecessary Type Conversion in Return StatementInformationalAcknowledged
Unnecessary Wrapper Function Adds Code BloatInformationalAcknowledged
Commented Out Code and Dead Code Indicates Code Quality IssuesInformationalAcknowledged
Documentation InconsistenciesInformationalAcknowledged
Inconsistent Error Handling PatternsInformationalAcknowledged
Debug Code Present in ProductionInformationalSolved - 07/16/2025
Inconsistent Naming Conventions For Fee and Tax ParametersInformationalAcknowledged

7. Findings & Tech Details

7.1 Missing Contract Address Update Functions Break Protocol When External Contracts Upgrade

//

High

Description

The CSPR.fun protocol is fundamentally dependent on external DEX contracts (router, factory, and WCSPR) for its core functionality. However, the contract lacks the ability to update these critical dependencies when they are upgraded or modified. The following setter functions are implemented in main.rs but lack corresponding entry point definitions in entry_points.rs:

  • set_wcspr_key()

  • set_dex_factory_key()

  • set_dex_router_contract_package_hash_key()

  • set_dex_router_contract_hash_key()


This architectural flaw creates a permanent rigidity that could break the protocol if any of these DEX contracts need to be upgraded. While DEX contracts are not frequently updated, there may be cases where security patches, feature additions, or optimizations require upgrades, making this vulnerability a potential risk that should be addressed.


When any of the DEX contracts (factory, router, or WCSPR) is upgraded, the CSPR.fun contract will continue to reference outdated contract addresses, rendering the entire protocol inoperable. This results in:

  1. Partial Token Creation Failure: Token creation is limited to tokens without initial buy amounts, as the contract cannot interact with updated WCSPR contract to process buys. However, these tokens become obsolete since they cannot be traded after creation

  2. Trading Functionality Destruction: Existing tokens cannot graduate to DEX trading, trapping users in the bonding curve

  3. User Fund Access Loss: Users will permanently lose access to their CSPR used to buy tokens through the contract, as those tokens can never graduate to become tradeable on DEX


This represents a complete failure of core contract functionality that threatens the economic viability of the entire platform. Unlike other vulnerabilities that may be exploited by malicious actors, this one will inevitably occur due to the natural evolution of DEX infrastructure, making it a guaranteed protocol failure.

Code Location

Below is the implementation of the generate_entry_points function, which is intended to include all publicly accessible entry points but currently falls short:

pub fn generate_entry_points() -> EntryPoints {
    let mut entry_points = generate_cep18_entry_points();
    entry_points.add_entry_point(init());
    entry_points.add_entry_point(set_is_paused());
    entry_points.add_entry_point(set_protocol_fee_recipient());
    entry_points.add_entry_point(set_protocol_fee_x100());
    entry_points.add_entry_point(set_reserved_tokens_for_graduation());
    entry_points.add_entry_point(set_creator_graduation_reward());
    entry_points.add_entry_point(set_token_initial_cspr_price());
    entry_points.add_entry_point(set_adjustment_factor_b());
    entry_points.add_entry_point(set_new_token_initial_supply());
    entry_points.add_entry_point(set_cspr_reserve_graduation_threshold());
    entry_points.add_entry_point(create());
    entry_points.add_entry_point(trade());
    entry_points.add_entry_point(get_trade_output());
    entry_points
}

Proof of Concept

The following test demonstrates that the set_wcspr_key method is not accessible as a public entry point, preventing the CSPR.fun contract administrator from updating critical external contract dependencies:

#[test]
#[should_panic = "NoSuchMethod"]
fn hal_unreachable_setter_methods_for_external_contracts() {
    let (mut builder, test_context) = setup();
    let sender = *DEFAULT_ACCOUNT_ADDR;

    call(
        &mut builder,
        &sender,
        test_context.csprfun_contract_hash.value(),
        "set_wcspr_key",
        runtime_args! { "wcspr_key" => test_context.wcspr_contract_hash.value() },
        false,
        0u64,
    );
}

Expected Results:

The test panics with NoSuchMethod error, demonstrating that:

  • The set_wcspr_key method is not exposed as a public entry point

  • The CSPR.fun contract administrator cannot update the WCSPR contract reference

  • No administrative function exists to update external contract dependencies

  • The contract is permanently bound to the initially deployed WCSPR contract address

Test Execution:

cargo test --package tests --lib -- hal_pocs::hal_unreachable_setter_methods_for_external_contracts --exact --show-output

Execution Result:



BVSS
Recommendation

Add corresponding entry point definitions in entry_points.rs for all implemented DEX-related setter functions to enable contract adaptability to DEX infrastructure changes.

Remediation Comment

SOLVED: The Friendly Market team solved this issue in the specified commit ID.

Remediation Hash

7.2 Incorrect Tax Calculation in Bonding Curve Leads to Reduced Output Amount, Protocol Fee Loss and Reserve Imbalances

//

High

Description

The bonding curve's trade function in contract/src/bonding_curve.rs incorrectly computes the amount_after_token_tax during sell operations by assuming that tax will always be applicable, without verifying if the caller is the designated tax recipient. This results in a discrepancy between the bonding curve’s calculations and the actual transfer behavior governed by CEP18 standards.


The CEP18 transfer_balance function applies tax only under the following conditions:

  1. tax_percentage > 0

  2. The recipient is either the liquidity pool (LP) pair or the CSPR Fun contract

  3. The sender is not the tax_recipient (this condition is omitted in the bonding curve calculations)


When the tax recipient sells tokens, they receive less CSPR than the proportional amount dictated by the tax rate, leading to the protocol collecting fewer fees (proportional to the tax rate), and causing the token reserve within the bonding curve to become misaligned with the actual token balance in the contract. This misalignment results in subsequent buys being overpriced because the recorded reserve appears lower than the actual balance.


For example, with a 5% tax rate and a 1% protocol fee, when the tax recipient sells 10,000 tokens:


The bonding curve incorrectly does the following:

  1. Deducts 5% tax (500 tokens) → 9,500 tokens

  2. Calculates 1% protocol fee on 9,500 → 95 tokens

  3. Uses 9,405 tokens for price calculation


Whereas it should:

  1. Not deduct any tax, maintaining the total at 10,000 tokens

  2. Calculate 1% protocol fee on 10,000 → 100 tokens

  3. Use 9,900 tokens for price calculation


This discrepancy results in:

  • Protocol receiving 5 fewer tokens in fees

  • The tax recipient receiving fewer CSPR, equivalent to 495 tokens less based on the tax rate

  • The token reserve showing 495 fewer tokens than the actual token balance

Code Location

Below is a code snippet of the trade function:

pub fn trade(&mut self, is_buy: bool, amount: U256, recipient: Key, min_amount: U256) -> U256 {
    if self.graduated {
        runtime::revert(CustomError::Graduated)
    }
    let caller = get_immediate_caller();
    runtime::print("before getting amount_after_token_tax ");
    // this is the amount this contract will receive
    let amount_after_token_tax = if is_buy {
        // no tax on buy
        amount
    } else {
        amount
            .checked_sub(
                amount
                    .checked_mul(get_cep18_tax_percentage(self.token_key))
                    .unwrap()
                    .checked_div(U256::from(10_000))
                    .unwrap(),
            )
            .unwrap()
    };
// rest of the code

Additionally, here is a snippet from the transfer_balance function within the CEP18 contract, which highlights that the tax_recipient is whitelisted from sale tax:

if tax_percentage > 0
    && (recipient == get_stored_value(LP_PAIR_KEY)
        || recipient == get_stored_value(CSPR_FUN_CONTRACT_PACKAGE_HASH_KEY))
    && (sender != get_stored_value(TAX_RECIPIENT_KEY))
    // tax recipient is whitelisted from tax
{
    // This is a sale. Tax is applied.
    // rest of the code
}

Proof of Concept

The following test demonstrates how the bonding curve incorrectly calculates tax when the tax recipient sells tokens, leading to reduced WCSPR returns and protocol fees:

#[test]
fn hal_incorrect_tax_calculation_in_bonding_curve() {
    // This test demonstrates a bug where tax is incorrectly calculated when the tax recipient sells tokens
    // The bonding curve contract deducts tax even though CEP18 transfers don't apply tax for the tax recipient
    // This leads to the tax recipient receiving less WCSPR than expected and protocol fees being reduced
    
    // Set up test environment and get contract references
    let (mut builder, test_context) = setup();

    let token_creator = *CREATOR_ACCOUNT_ADDR;
    let contract_hash = test_context.csprfun_contract_hash;
    let csprfun_contract_package_hash = test_context.csprfun_contract_package_hash;
    let wcspr_contract_hash = test_context.wcspr_contract_hash;

    // Set up token metadata for creation
    let name: String = "CSPR.fun".to_string();
    let symbol: String = "CSF".to_string();
    let descrption: String = "CSPR.fun token".to_string();
    let logo: String = "https://cspr.fun/logo.png".to_string();
    let website: String = "https://cspr.fun/logo.png".to_string();
    let twitter: String = "https://cspr.fun/logo.png".to_string();
    let discord: String = "https://cspr.fun/logo.png".to_string();
    let telegram: String = "https://cspr.fun/logo.png".to_string();
    let token_additional_metadata: String = "https://cspr.fun/logo.png".to_string();

    // Configure token with 5% tax where tax recipient is the creator
    let tax_percentage_x100 = 500u32; // 5%
    let tax_recipient = token_creator;
    let buy_amount = U256::from(1_000_000_000); // Initial buy of 1 Token

    // Fund creator with WCSPR for initial buy
    erc20_transfer(
        &mut builder,
        &DEFAULT_ACCOUNT_ADDR,
        wcspr_contract_hash,
        Key::from(token_creator),
        buy_amount,
    );

    let creator_balance_of_wcspr_before_buy = erc20_balance_of(
        &mut builder,
        Key::from(token_creator),
        wcspr_contract_hash.value().into(),
    );

    // Approve WCSPR spending for token creation with initial buy
    erc20_approve(
        &mut builder,
        &token_creator,
        wcspr_contract_hash,
        Key::from(ContractPackageHash::from(
            csprfun_contract_package_hash.value(),
        )),
        buy_amount,
    );

    // Step 1: Create token and execute initial buy
    // This establishes the initial bonding curve state and token balances
    let token_contract_hash = create(
        &mut builder,
        &token_creator,
        contract_hash,
        name.clone(),
        symbol.clone(),
        descrption.clone(),
        logo.clone(),
        website.clone(),
        twitter.clone(),
        discord.clone(),
        telegram.clone(),
        token_additional_metadata.clone(),
        Some(buy_amount),
        Some(tax_percentage_x100),
        Some(Key::from(tax_recipient)),
    );

    // Calculate expected protocol fees and output amounts for initial buy
    let expected_fees = buy_amount
        .checked_mul(get_protocol_fee_x100(&mut builder, contract_hash))
        .unwrap()
        .checked_div(U256::from(10_000))
        .unwrap();

    let expected_amount_in_after_fees = buy_amount.checked_sub(expected_fees).unwrap(); // 990000000
    let expected_amount_out = U256::from(1100000000000u64);

    // Approve additional WCSPR for trading
    erc20_approve(
        &mut builder,
        &token_creator,
        wcspr_contract_hash.into(),
        Key::from(ContractPackageHash::from(
            csprfun_contract_package_hash.value(),
        )),
        buy_amount,
    );

    // Get state after initial buy to verify correct setup
    let bonding_curve_after_buy =
        get_bonding_curve_for_token(&mut builder, contract_hash, token_contract_hash);

    let creator_balance_after_buy = erc20_balance_of(
        &mut builder,
        Key::from(token_creator),
        token_contract_hash.into_hash_addr().unwrap().into(),
    );

    let creator_balance_of_wcspr_after_buy = erc20_balance_of(
        &mut builder,
        Key::from(token_creator),
        wcspr_contract_hash.value().into(),
    );

    let csprfun_balance_after_buy = erc20_balance_of(
        &mut builder,
        Key::Hash(test_context.csprfun_contract_package_hash.value()),
        token_contract_hash.into_hash_addr().unwrap().into(),
    );
    let csprfun_balance_of_wcspr_after_buy = erc20_balance_of(
        &mut builder,
        Key::Hash(test_context.csprfun_contract_package_hash.value()),
        wcspr_contract_hash.value().into(),
    );

    // Verify initial buy deducted correct WCSPR amount from creator
    assert_eq!(
        creator_balance_of_wcspr_after_buy,
        creator_balance_of_wcspr_before_buy
            .checked_sub(buy_amount)
            .unwrap()
    );

    // Verify contract received correct token amount after initial buy
    let csprfun_balance_after_buy = erc20_balance_of(
        &mut builder,
        Key::Hash(test_context.csprfun_contract_package_hash.value()),
        token_contract_hash.into_hash_addr().unwrap().into(),
    );

    assert_eq!(
        csprfun_balance_after_buy,
        U256::from(DEFAULT_NEW_TOKEN_INITIAL_SUPPLY)
            .checked_sub(expected_amount_out)
            .unwrap()
    );

    // Verify contract received correct WCSPR amount after fees
    let csprfun_balance_of_wcspr_after_buy = erc20_balance_of(
        &mut builder,
        Key::Hash(test_context.csprfun_contract_package_hash.value()),
        wcspr_contract_hash.value().into(),
    );

    assert_eq!(
        csprfun_balance_of_wcspr_after_buy,
        expected_amount_in_after_fees
    );

    assert!(!bonding_curve_after_buy.graduated);

    // Step 2: Execute sell trade by tax recipient to demonstrate incorrect tax calculation
    // The bonding curve will incorrectly deduct tax even though the seller is the tax recipient
    let sell_amount = bonding_curve_after_buy.cspr_reserve; // 989153550

    // Approve token spending for sell
    erc20_approve(
        &mut builder,
        &token_creator,
        token_contract_hash.into_hash_addr().unwrap().into(),
        Key::from(ContractPackageHash::from(
            csprfun_contract_package_hash.value(),
        )),
        sell_amount,
    );

    // Track protocol fee recipient balance before sell to verify fee calculation
    let protocol_fee_recipient_balance_before_sell = erc20_balance_of(
        &mut builder,
        Key::from(test_context.fee_recipient),
        token_contract_hash.into_hash_addr().unwrap().into(),
    );

    // Execute sell trade - this will demonstrate the tax calculation bug
    trade(
        &mut builder,
        &token_creator,
        contract_hash,
        token_contract_hash,
        Key::from(token_creator),
        sell_amount,
        U256::zero(), // Disable slippage protection
        false,
    );

    // Get final state after sell to verify incorrect calculations
    let bonding_curve_after_sell =
        get_bonding_curve_for_token(&mut builder, contract_hash, token_contract_hash);

    let csprfun_balance_of_wcspr_after_sell = erc20_balance_of(
        &mut builder,
        Key::Hash(test_context.csprfun_contract_package_hash.value()),
        wcspr_contract_hash.value().into(),
    );
    let expected_fees = sell_amount
        .checked_mul(get_protocol_fee_x100(&mut builder, contract_hash))
        .unwrap()
        .checked_div(U256::from(10_000))
        .unwrap();
    let expected_wcspr_received = sell_amount.checked_sub(expected_fees).unwrap();

    // Verify bonding curve state shows incorrect calculations
    assert!(
        bonding_curve_after_sell.cspr_reserve < bonding_curve_after_buy.cspr_reserve,
        "cspr_reserve decreased"
    );
    assert!(
        bonding_curve_after_sell
            .tokens_reserve
            .checked_sub(bonding_curve_after_buy.tokens_reserve)
            .unwrap()
            < sell_amount.checked_sub(expected_fees).unwrap(),
        "tokens_reserve increased by less than the expected amount"
    );
    assert_eq!(
        csprfun_balance_of_wcspr_after_sell, bonding_curve_after_sell.cspr_reserve,
        "cspr_reserve is not equal to csprfun balance of wcspr"
    );

    let csprfun_balance_after_sell = erc20_balance_of(
        &mut builder,
        Key::Hash(test_context.csprfun_contract_package_hash.value()),
        token_contract_hash.into_hash_addr().unwrap().into(),
    );
    assert!(
        bonding_curve_after_sell.tokens_reserve < csprfun_balance_after_sell,
        "tokens_reserve is not less than csprfun balance"
    );

    // Verify tax recipient (creator) received incorrect WCSPR amount due to tax being wrongly applied
    let creator_balance_after_sell = erc20_balance_of(
        &mut builder,
        Key::from(token_creator),
        token_contract_hash.into_hash_addr().unwrap().into(),
    );

    assert_eq!(
        creator_balance_after_sell,
        creator_balance_after_buy.checked_sub(sell_amount).unwrap(),
        "creator balance decreased by sell_amount"
    );

    let creator_balance_of_wcspr_after_sell = erc20_balance_of(
        &mut builder,
        Key::from(token_creator),
        wcspr_contract_hash.value().into(),
    );

    // Key assertion showing the bug: creator received less WCSPR than they should have
    assert!(
        creator_balance_of_wcspr_after_sell - creator_balance_of_wcspr_after_buy
            < expected_wcspr_received,
        "creator (tax recipient) got less wcspr than expected"
    );

    // Verify protocol fee recipient also received less than expected due to incorrect calculation
    let protocol_fee_recipient_balance_after_sell = erc20_balance_of(
        &mut builder,
        Key::from(test_context.fee_recipient),
        token_contract_hash.into_hash_addr().unwrap().into(),
    );

    assert!(
        protocol_fee_recipient_balance_after_sell
            .checked_sub(protocol_fee_recipient_balance_before_sell)
            .unwrap()
            < expected_fees,
        "protocol fee recipient received less tokens than expected"
    );
}

Expected Results:

The test passes, demonstrating that:

  • A token is created with 5% tax where the creator is the tax recipient

  • Initial buy establishes proper bonding curve state

  • When the tax recipient (creator) sells tokens, the bonding curve incorrectly deducts tax

  • The tax recipient receives less WCSPR than expected due to double tax deduction

  • Protocol fees are also reduced due to the incorrect calculation

  • The bonding curve state becomes inconsistent with actual token balances


Test Execution:

cargo test --package tests --lib -- hal_pocs::hal_incorrect_tax_calculation_in_bonding_curve --exact --show-output

Execution Result:



BVSS
Recommendation

It is recommended to add a check in the bonding curve's trade function to verify if the caller is the tax recipient before calculating the assumed tax amount.

Remediation Comment

SOLVED: The Friendly Market team successfully remediated the issue by adding a check to verify if the caller is the tax recipient before calculating the assumed tax amount.

Remediation Hash

7.3 Malicious Tax Configuration Enables DEX Pool Drainage and Blocks WCSPR Returns

//

Low

Description

The CSPR.fun protocol enables users to create and trade tokens via a bonding curve mechanism prior to transitioning to DEX trading. The process works as follows:


1. Token Creation:

- The creator defines token parameters, including the tax percentage and tax recipient.

- An initial token supply is minted to the bonding curve contract.

- The contract holds CSPR and token reserves to facilitate trading.


2. Pre-Graduation Trading:

- Users can purchase tokens by sending CSPR to the contract.

- Users can sell tokens back to the contract for CSPR.

- Prices are determined by the bonding curve based on reserve ratios.

- The contract accumulates CSPR from token sales.

- Tax applies only on sales, not on purchases.


3. Graduation:

- When the contract accumulates sufficient CSPR (graduation threshold),

- The contract automatically graduates the token.

- Reserves are used to create a DEX liquidity pool.

- The token becomes available for trading on the DEX.


However, a critical vulnerability exists within this mechanism. The tax percentage parameter currently lacks validation, allowing malicious actors to set a 100% tax rate, which can be exploited as follows:


1. The attacker creates a token with:

- A 100% tax rate (equivalent to 10,000 basis points),

- The tax recipient set to their own address.


2. Prior to graduation:

- Users can purchase tokens without tax implications.

- When users attempt to sell tokens back to the bonding curve:

- The 100% tax diverts all tokens to the attacker’s address.

- No tokens remain after the tax is applied.

- Users receive no WCSPR, as the output depends on the post-tax amount.


3. After graduation and listing on the DEX:

- Users trading this token on the DEX will have all tokens diverted to the attacker due to the 100% tax.

- The DEX liquidity pool releases other tokens but receives nothing in return.

- The attacker can repeatedly drain the pool's reserves without providing any counter-value.


This issue effectively enables malicious tokens to behave like honeypots, where users who attempt to sell, as well as the protocol fee recipient, receive nothing. This risk is especially pronounced for casual traders who often skip setting a minimum output amount, leaving them vulnerable to receiving zero tokens in return. The attacker, acting as the tax recipient, can then sell the collected taxed tokens and drain the corresponding WCSPR reserves from the bonding curve.


Both the bonding curve contract and DEX allow traders to specify minimum output amounts that will revert trades if not met. However, users who don't specify a minimum output amount in their trade parameters remain at risk, as trades will still execute even when 100% tax results in near-zero output tokens. This allows systematic draining of liquidity without providing meaningful counter-value, severely impacting protocol stability and liquidity providers.

Proof of Concept

The following test demonstrates how a malicious token creator can create a token with 100% tax and drain all trading value:

#[test]
fn hal_create_token_with_excessive_tax() {
    // Set up test environment and get contract references
    let (mut builder, test_context) = setup();

    let token_creator = *DEFAULT_ACCOUNT_ADDR;
    let contract_hash = test_context.csprfun_contract_hash;
    let csprfun_contract_package_hash = test_context.csprfun_contract_package_hash;
    let wcspr_contract_hash = test_context.wcspr_contract_hash;

    // Token metadata
    let name: String = "CSPR.fun".to_string();
    let symbol: String = "CSF".to_string();
    let descrption: String = "CSPR.fun token".to_string();
    let logo: String = "https://cspr.fun/logo.png".to_string();
    let website: String = "https://cspr.fun/logo.png".to_string();
    let twitter: String = "https://cspr.fun/logo.png".to_string();
    let discord: String = "https://cspr.fun/logo.png".to_string();
    let telegram: String = "https://cspr.fun/logo.png".to_string();
    let token_additional_metadata: String = "https://cspr.fun/logo.png".to_string();

    // Set up malicious tax parameters - 100% tax means all tokens go to tax recipient
    let tax_percentage_x100 = 10_000u32; // 100%
    let tax_recipient = token_creator;

    // 1 - Malicious token creator creates a token with 100% tax
    let token_contract_hash = create(
        &mut builder,
        &token_creator,
        contract_hash,
        name.clone(),
        symbol.clone(),
        descrption.clone(),
        logo.clone(),
        website.clone(),
        twitter.clone(),
        discord.clone(),
        telegram.clone(),
        token_additional_metadata.clone(),
        None,
        Some(tax_percentage_x100),
        Some(Key::from(tax_recipient)),
    );

    // Set up trader account (who is also the protocol fee recipient)
    let trader = *ACCOUNT_2_ADDR;
    assert_eq!(trader, test_context.fee_recipient);

    // Configure trade parameters
    let is_buy: bool = true;
    let buy_amount = U256::from(1_000_000_000); // 1 Token
    let min_receive_amount = U256::from(1_000_000_000); // 1 Token
    let recipient = Key::from(trader);

    // Transfer WCSPR to trader for trading
    erc20_transfer(
        &mut builder,
        &token_creator,
        wcspr_contract_hash,
        Key::from(trader),
        buy_amount,
    );

    // Calculate expected fees and amounts
    let expected_fees = buy_amount
        .checked_mul(get_protocol_fee_x100(&mut builder, contract_hash))
        .unwrap()
        .checked_div(U256::from(10_000))
        .unwrap();

    let expected_amount_in_after_fees = buy_amount.checked_sub(expected_fees).unwrap(); // 990000000
    let expected_amount_out = U256::from(1100000000000u64);

    // Approve WCSPR spending
    erc20_approve(
        &mut builder,
        &trader,
        wcspr_contract_hash.into(),
        Key::from(ContractPackageHash::from(
            csprfun_contract_package_hash.value(),
        )),
        buy_amount,
    );

    // Get initial state before first trade
    let bonding_curve_before_trade_1 =
        get_bonding_curve_for_token(&mut builder, contract_hash, token_contract_hash);

    let user_balance_before_trade_1 = erc20_balance_of(
        &mut builder,
        Key::from(trader),
        token_contract_hash.into_hash_addr().unwrap().into(),
    );

    let user_balance_of_wcspr_before_trade_1 = erc20_balance_of(
        &mut builder,
        Key::from(trader),
        wcspr_contract_hash.value().into(),
    );

    let csprfun_balance_before_trade_1 = erc20_balance_of(
        &mut builder,
        Key::Hash(test_context.csprfun_contract_package_hash.value()),
        token_contract_hash.into_hash_addr().unwrap().into(),
    );
    let csprfun_balance_of_wcspr_before_trade_1 = erc20_balance_of(
        &mut builder,
        Key::Hash(test_context.csprfun_contract_package_hash.value()),
        wcspr_contract_hash.value().into(),
    );

    // 2 - Execute buy trade
    trade(
        &mut builder,
        &trader,
        contract_hash,
        token_contract_hash,
        recipient,
        buy_amount,
        min_receive_amount,
        is_buy,
    );

    // Verify bonding curve state after buy
    let bonding_curve_after_trade_1 =
        get_bonding_curve_for_token(&mut builder, contract_hash, token_contract_hash);

    assert_eq!(
        bonding_curve_after_trade_1.cspr_reserve,
        bonding_curve_before_trade_1
            .cspr_reserve
            .checked_add(expected_amount_in_after_fees)
            .unwrap()
    );

    assert_eq!(
        bonding_curve_after_trade_1.tokens_reserve,
        bonding_curve_before_trade_1
            .tokens_reserve
            .checked_sub(expected_amount_out)
            .unwrap()
    );
    assert!(!bonding_curve_after_trade_1.graduated);

    // Verify trader token balance after buy
    let user_balance_after_trade_1 = erc20_balance_of(
        &mut builder,
        Key::from(trader),
        token_contract_hash.into_hash_addr().unwrap().into(),
    );

    assert_eq!(
        user_balance_after_trade_1,
        user_balance_before_trade_1
            .checked_add(expected_amount_out)
            .unwrap()
    );

    // Verify trader WCSPR balance after buy
    let user_balance_of_wcspr_after_trade_1 = erc20_balance_of(
        &mut builder,
        Key::from(trader),
        wcspr_contract_hash.value().into(),
    );

    assert_eq!(
        user_balance_of_wcspr_after_trade_1,
        user_balance_of_wcspr_before_trade_1
            .checked_sub(buy_amount)
            .unwrap()
            .checked_add(expected_fees) // Trader receives protocol fee since they are fee recipient
            .unwrap()
    );

    // Verify CSPR.fun contract token balance after buy
    let csprfun_balance_after_trade_1 = erc20_balance_of(
        &mut builder,
        Key::Hash(test_context.csprfun_contract_package_hash.value()),
        token_contract_hash.into_hash_addr().unwrap().into(),
    );

    assert_eq!(
        csprfun_balance_after_trade_1,
        csprfun_balance_before_trade_1
            .checked_sub(expected_amount_out)
            .unwrap()
    );

    // Verify CSPR.fun contract WCSPR balance after buy
    let csprfun_balance_of_wcspr_after_trade_1 = erc20_balance_of(
        &mut builder,
        Key::Hash(test_context.csprfun_contract_package_hash.value()),
        wcspr_contract_hash.value().into(),
    );

    assert_eq!(
        csprfun_balance_of_wcspr_after_trade_1,
        csprfun_balance_of_wcspr_before_trade_1
            .checked_add(expected_amount_in_after_fees)
            .unwrap()
    );

    assert!(!bonding_curve_after_trade_1.graduated);

    // 3 - Execute sell trade to demonstrate tax effect
    // Get creator's token balance before sell
    let creator_balance_of_token_before_trade_2 = erc20_balance_of(
        &mut builder,
        Key::from(token_creator),
        token_contract_hash.into_hash_addr().unwrap().into(),
    );

    let sell_amount = U256::from(1_000_000_000); // 1 Token

    // Approve token spending for sell
    erc20_approve(
        &mut builder,
        &trader,
        token_contract_hash.into_hash_addr().unwrap().into(),
        Key::from(ContractPackageHash::from(
            csprfun_contract_package_hash.value(),
        )),
        sell_amount,
    );

    // Execute sell trade
    trade(
        &mut builder,
        &trader,
        contract_hash,
        token_contract_hash,
        recipient,
        sell_amount,
        U256::zero(), // Disable slippage protection
        false,
    );

    // Verify bonding curve state unchanged after sell (due to 100% tax)
    let bonding_curve_after_trade_2 =
        get_bonding_curve_for_token(&mut builder, contract_hash, token_contract_hash);

    assert_eq!(
        bonding_curve_after_trade_2.cspr_reserve, bonding_curve_after_trade_1.cspr_reserve,
        "cspr_reserve didn't change"
    );
    assert_eq!(
        bonding_curve_after_trade_2.tokens_reserve, bonding_curve_after_trade_1.tokens_reserve,
        "tokens_reserve didn't change"
    );

    // Verify trader lost tokens but received no WCSPR (due to 100% tax)
    let user_balance_after_trade_2 = erc20_balance_of(
        &mut builder,
        Key::from(trader),
        token_contract_hash.into_hash_addr().unwrap().into(),
    );

    assert_eq!(user_balance_after_trade_2, user_balance_after_trade_1.checked_sub(sell_amount).unwrap(), "user balance decreased by sell_amount - as the protocol recipient, they got zero tokens from the trade");

    let user_balance_of_wcspr_after_trade_2 = erc20_balance_of(
        &mut builder,
        Key::from(trader),
        wcspr_contract_hash.value().into(),
    );

    assert_eq!(
        user_balance_of_wcspr_after_trade_2, user_balance_of_wcspr_after_trade_1,
        "user didn't get any WCSPR back"
    );

    // Verify CSPR.fun contract balances unchanged
    let csprfun_balance_after_trade_2 = erc20_balance_of(
        &mut builder,
        Key::Hash(test_context.csprfun_contract_package_hash.value()),
        token_contract_hash.into_hash_addr().unwrap().into(),
    );

    assert_eq!(
        csprfun_balance_after_trade_2, csprfun_balance_after_trade_1,
        "csprfun token balance didn't change"
    );

    let csprfun_balance_of_wcspr_after_trade_2 = erc20_balance_of(
        &mut builder,
        Key::Hash(test_context.csprfun_contract_package_hash.value()),
        wcspr_contract_hash.value().into(),
    );

    assert_eq!(
        csprfun_balance_of_wcspr_after_trade_2, csprfun_balance_of_wcspr_after_trade_1,
        "csprfun balance of WCSPR didn't change"
    );

    // Verify creator received all sold tokens due to 100% tax
    let creator_balance_of_token_after_trade_2 = erc20_balance_of(
        &mut builder,
        Key::from(token_creator),
        token_contract_hash.into_hash_addr().unwrap().into(),
    );

    assert_eq!(
        creator_balance_of_token_after_trade_2,
        creator_balance_of_token_before_trade_2
            .checked_add(sell_amount)
            .unwrap(),
        "creator balance of token increased by sell_amount as they got the tax"
    );
}

Expected Results:

The test passes, demonstrating that:

  • A token creator can successfully create a token with 100% tax

  • Traders can buy tokens normally (initial liquidity provision works)

  • When traders attempt to sell tokens, they lose their tokens but receive no WCSPR

  • All sold tokens are transferred to the tax recipient (token creator)

  • The bonding curve state remains unchanged during sells due to 100% tax

Test Execution:

cargo test --package tests --lib -- hal_pocs::hal_create_token_with_excessive_tax --exact --show-output

Execution Result:



BVSS
Recommendation

Add validation in the token creation process to enforce a reasonable upper bound for the tax percentage. The tax percentage should be validated to ensure it cannot exceed a safe maximum (e.g. 10% or 1000 basis points). This validation should be added in the initialization function where the tax percentage is set.

Remediation Comment

RISK ACCEPTED: The Friendly Market team accepted the risk of this finding. In their response, they emphasized a commitment to fostering flexibility and innovation within their protocol, particularly in how token tax mechanisms are configured. They highlighted that:


“We’ve seen numerous tokens with 50%+ tax get traction and generate millions of volume. What if the tax recipient of a high tax token is another protocol? Or a burn address? These are both valid, real-world examples of where high-tax tokens are used.”


Additionally, the client noted that token tax percentages will be transparently displayed in the user interface, ensuring users are fully informed before engaging with any token. Their overarching goal is to create a platform that encourages creative tokenomic designs rather than imposing rigid constraints:


“What we’re trying to do here is build a product that will actually encourage creativity, not limit it.”


As such, while the concern around high tax thresholds remains valid from a risk perspective, the client has made an intentional product decision to prioritize design flexibility over strict guardrails in this area.

7.4 Missing Getter Functions Lead to Blind Trading

//

Low

Description

The CSPR.fun contract has multiple getter functions implemented in main.rs that lack corresponding entry point definitions in entry_points.rs, making them inaccessible from outside the contract. The following getter functions exist in main.rs but have no entry point definitions:

  • get_is_paused()

  • get_new_token_initial_supply()

  • get_reserved_tokens_for_graduation()

  • get_cspr_reserve_graduation_threshold()

  • get_adjustment_factor_b()

  • get_protocol_fee_recipient()

  • get_protocol_fee_x100()

  • get_token_initial_cspr_price()

  • get_creator_graduation_reward()

  • get_wcspr_key()

  • get_dex_factory_key()

  • get_dex_router_contract_package_hash_key()

  • get_dex_router_contract_hash_key()


Without proper entry point definitions, these functions cannot be called from outside the contract, severely limiting the contract's transparency and usability. Users are forced to make "blind" transactions without being able to verify critical contract state like pause status, current token prices, expected slippage, or protocol fees. This lack of visibility into contract parameters prevents users from making informed decisions and could lead to unexpected transaction outcomes or hidden costs that users cannot anticipate beforehand.


For example, users cannot:

  • Check if the contract is paused before attempting trades

  • Verify current protocol fees to calculate expected costs

  • Determine token prices to estimate slippage

  • Access bonding curve parameters to understand pricing mechanics


This creates significant usability and transparency issues that force users to make uninformed decisions, potentially leading to suboptimal trades, unexpected costs, or failed transactions.

Code Location

Below is the implementation of the generate_entry_points function, which is intended to include all publicly accessible entry points but currently falls short:

pub fn generate_entry_points() -> EntryPoints {
    let mut entry_points = generate_cep18_entry_points();
    entry_points.add_entry_point(init());
    entry_points.add_entry_point(set_is_paused());
    entry_points.add_entry_point(set_protocol_fee_recipient());
    entry_points.add_entry_point(set_protocol_fee_x100());
    entry_points.add_entry_point(set_reserved_tokens_for_graduation());
    entry_points.add_entry_point(set_creator_graduation_reward());
    entry_points.add_entry_point(set_token_initial_cspr_price());
    entry_points.add_entry_point(set_adjustment_factor_b());
    entry_points.add_entry_point(set_new_token_initial_supply());
    entry_points.add_entry_point(set_cspr_reserve_graduation_threshold());
    entry_points.add_entry_point(create());
    entry_points.add_entry_point(trade());
    entry_points.add_entry_point(get_trade_output());
    entry_points
}

Proof of Concept

The following test demonstrates that the get_protocol_fee_x100 method is not accessible as a public entry point, despite being implemented by the csprfun contract:

#[test]
#[should_panic = "NoSuchMethod"]
fn hal_unreachable_getter_methods() {
    let (mut builder, test_context) = setup();
    let sender = *DEFAULT_ACCOUNT_ADDR;

    call(
        &mut builder,
        &sender,
        test_context.csprfun_contract_hash.value(),
        "get_protocol_fee_x100",
        runtime_args! {},
        false,
        0u64,
    );
}

Expected Results:

The test panics with NoSuchMethod error, demonstrating that:

  • The get_protocol_fee_x100 method exists and is used internally by the contract

  • The method is not exposed as a public entry point for external calls

  • Users cannot query the current protocol fee rate through the contract interface

  • This creates a lack of transparency and makes it difficult for users to verify fee calculations


Test Execution:

cargo test --package tests --lib -- hal_pocs::hal_unreachable_getter_methods --exact --show-output

Execution Result:



BVSS
Recommendation

It is recommended to add corresponding entry point definitions in entry_points.rs for all implemented getter functions to enable proper contract transparency and user decision-making.

Remediation Comment

SOLVED: The Friendly Market team has added individual public entry-point definitions for every getter function and included all of them in generate_entry_points(). These getters are now callable from outside the contract, fully restoring transparency and eliminating the original usability risk:

  • get_is_paused

  • get_new_token_initial_supply

  • get_reserved_tokens_for_graduation

  • get_cspr_reserve_graduation_threshold

  • get_adjustment_factor_b

  • get_protocol_fee_recipient

  • get_protocol_fee_x100

  • get_token_initial_cspr_price

  • get_creator_graduation_reward

  • get_wcspr_key

  • get_dex_factory_key

  • get_dex_router_contract_package_hash_key

  • get_dex_router_contract_hash_key


Remediation Hash

7.5 Excessive Token Approvals in DEX Liquidity Addition

//

Low

Description

The add_dex_liquidity function grants approval of U256::MAX tokens for both WCSPR and the bonding curve token to the DEX router . This allows for a one-time liquidity addition to enable trading on the DEX once the tokens reach a specified graduation threshold.


This approval gives the DEX router unlimited spending authority over all token balances held by the contract. Although the contract intends to approve only specific amounts (amount_wcspr and amount_token), the approval remains active and could be exploited if the router is compromised or misused.


A compromised or malicious DEX router could potentially drain the entire token balances of the contract, resulting in substantial financial losses. The approval persists indefinitely, increasing the risk window beyond the initial liquidity addition event.


This security concern also applies to the wrap-approve-trade session code, which grants the CSPR.fun contract an allowance of U256::MAX every time it runs. If the CSPR.fun contract contains a bug or is compromised, it could transfer the caller’s entire WCSPR / token balance without further approval. This exposes users to unnecessary risk and violates the principle of least privilege.

Code Location

Below is the implementation of the add_dex_liquidity function:

pub fn add_dex_liquidity(
    token_key: Key,
    amount_wcspr: U256,
    amount_token: U256,
) -> ContractPackageHash {
    let wcspr: Key = get_wcspr_key();
    runtime::call_contract::<()>(
        ContractHash::from(wcspr.into_hash_addr().unwrap_or_revert()),
        ENTRY_POINT_APPROVE,
        runtime_args! {
            ARG_SPENDER => get_dex_router_contract_package_hash_key(),
            ARG_AMOUNT => U256::MAX,
        },
    );
    runtime::call_contract::<()>(
        ContractHash::from(token_key.into_hash_addr().unwrap_or_revert()),
        ENTRY_POINT_APPROVE,
        runtime_args! {
            ARG_SPENDER => get_dex_router_contract_package_hash_key(),
            ARG_AMOUNT => U256::MAX,
        },
    );
    runtime::call_contract::<()>(
        ContractHash::from(
            get_dex_router_contract_hash_key()
                .into_hash_addr()
                .unwrap_or_revert(),
        ),
        ADD_LIQUIDITY,
        runtime_args! {
            TOKEN_A_RUNTIME_ARG_NAME => wcspr,
            TOKEN_B_RUNTIME_ARG_NAME => token_key,
            AMOUNT_A_DESIRED_RUNTIME_ARG_NAME => amount_wcspr,
            AMOUNT_B_DESIRED_RUNTIME_ARG_NAME => amount_token,
            AMOUNT_A_MIN_RUNTIME_ARG_NAME => U256::zero(),
            AMOUNT_B_MIN_RUNTIME_ARG_NAME => U256::zero(),
            TO_RUNTIME_ARG_NAME => Key::Account(AccountHash::default()),
            DEADLINE_RUNTIME_ARG_NAME => u64::from(runtime::get_blocktime()),
        },
    );

    let (pair, _): (ContractPackageHash, u32) = pair_for(
        ContractHash::from(get_dex_factory_key().into_hash_addr().unwrap()),
        wcspr,
        token_key,
    );
    pair
}

BVSS
Recommendation

It is recommended to approve only the exact amounts needed for the liquidity addition (amount_wcspr and amount_token) and for the trade approval in the session code.

Remediation Comment

PARTIALLY SOLVED: The unlimited-allowance vulnerability has been addressed in the add_dex_liquidity  function. The  contract now  approves  only the  exact  liquidity  amounts instead of using U256::MAX. However, this risk remains present in the wrap-approve-trade session code.

Remediation Hash

7.6 Missing Getter Function for Token Metadata

//

Low

Description

The CEP18 token contract stores token metadata during initialization but lacks a corresponding getter function to retrieve this information. While the contract provides getter functions for basic token properties like name(), symbol(), decimals(), and total_supply(), there is no equivalent function for accessing the token metadata.


Token metadata typically contains important information such as:

  • Project description and website links

  • Social media handles (Twitter, Discord, Telegram)

  • Logo URLs and additional branding information

  • Custom project-specific data


This missing functionality prevents external applications, wallets, and DEX interfaces from displaying comprehensive token information to users, limiting the token's discoverability and user experience. Frontend applications and blockchain explorers rely on these metadata getters to provide rich token information displays.

BVSS
Recommendation

It is recommended to add a token_metadata() entry point function following the same pattern as other getter functions.

This will enable external applications to retrieve and display complete token information, improving the overall ecosystem integration and user experience.

Remediation Comment

SOLVED: The CEP-18 implementation already exposes token metadata through a dedicated public entry point.

Remediation Hash

7.7 Missing Upper Limit Validation for Protocol Fee Configuration

//

Low

Description

The set_protocol_fee_x100 function in contract/src/utils.rs lacks validation for the maximum protocol fee percentage, allowing administrators to set arbitrarily high protocol fees that could drain user funds or make the platform economically unviable.


The protocol fee is applied to all token trades and affects the economic viability of the entire platform. Without proper bounds checking, the admin has unchecked power to set fees that could effectively shut down the platform or extract unreasonable value from users.


Note: While users can set a minimum output amount to protect against high fees, those who do not are at risk. Excessive fees can also cause most trades to fail, impacting platform usability.

Code Location

Below is the implementation of the set_protocol_fee_x100 function:

#[no_mangle]
pub extern "C" fn set_protocol_fee_x100() {
    revert_if_not_admin();

    utils::set_protocol_fee_x100(runtime::get_named_arg::<U256>(PROTOCOL_FEE_X100));
}

Below is the implementation of the utils::set_protocol_fee_x100 function:

pub fn set_protocol_fee_x100(protocol_fee_x100: U256) {
    named_dictionary_put(GLOBAL_DICT, PROTOCOL_FEE_X100, protocol_fee_x100);
}

BVSS
Recommendation

It is recommended to add validation in the set_protocol_fee_x100 function to enforce a reasonable upper bound for the protocol fee. The fee should be validated to ensure it cannot exceed a safe maximum (e.g., 10% or 1000 basis points). This validation should prevent the admin from setting fees that would make the platform economically unviable or allow excessive fund extraction.

Remediation Comment

NOT SOLVED: The contract still lets an admin set an unlimited protocol-fee value.

7.8 Missing Zero Amount Validation in Trade Function

//

Informational

Description

The trade function in contract/src/bonding_curve.rs lacks short-circuiting validation to ensure the amount parameter is greater than zero. This missing validation allows users to call the trade function with zero amounts, resulting in unnecessary gas consumption as the function executes all calculations, state updates, and event emissions even when no actual trade occurs.


The function performs expensive operations including tax calculations, protocol fee computations, bonding curve price calculations, and reserve updates, all of which consume gas without providing any value when the input amount is zero.


Additionally, trade events are recorded for zero-amount trades, creating noise in event logs and potentially misleading external systems monitoring the contract. While the function includes a minimum output check (if output < min_amount), this only prevents trades that would result in insufficient output, but doesn't prevent the execution of trades with zero input amounts.

BVSS
Recommendation

It is recommended to add input validation at the beginning of the trade function to check that amount > 0 and revert with an appropriate error if the condition is not met.

Remediation Comment

ACKNOWLEDGED: The Friendly Market team has acknowledged this finding.

7.9 Lacking Event Emissions for Critical Parameter Changes

//

Informational

Description

The contract implements multiple admin-only setter functions that modify critical protocol parameters without emitting corresponding events.


The following setter functions in the main contract lack event emissions:

  • set_is_paused()

  • set_new_token_initial_supply()

  • set_reserved_tokens_for_graduation()

  • set_cspr_reserve_graduation_threshold()

  • set_adjustment_factor_b()

  • set_wcspr_key()

  • set_dex_factory_key()

  • set_dex_router_contract_package_hash_key()

  • set_dex_router_contract_hash_key()

  • set_protocol_fee_recipient()

  • set_protocol_fee_x100()

  • set_token_initial_cspr_price()

  • set_creator_graduation_reward()


Additionally, the CEP18 contract's set_lp_pair_key() function also lacks event emission.


This lack of transparency prevents external monitoring systems, frontend applications, and users from being notified of important contract state changes.

BVSS
Recommendation

It is recommended to emit an event whenever important contract state variables are updated.

Remediation Comment

ACKNOWLEDGED: The Friendly Market team has acknowledged this finding.

7.10 Missing Input Validation For Graduation Parameters

//

Informational

Description

The contract lacks validation to ensure that graduate parameters are logically consistent. Specifically, there are no checks to verify that:


  1. creator_graduation_reward_cspr is less than cspr_reserves_graduation_threshold, and

  2. reserved_tokens_for_graduation is less than new_token_initial_supply.


If an admin configures these parameters incorrectly during contract initialization or through the setter functions, the graduate function will revert with an underflow error when attempting to subtract the creator reward from the CSPR reserve , or the create function will revert when subtracting reserved tokens from the initial supply. This could permanently block the graduation mechanism and token creation functionality if the admin sets invalid values.


Code Location

Code of graduate function from contract/src/bonding_curve.rs file:

fn graduate(&mut self) {
    self.graduated = true;
    self.write_to_storage();

    cep18_transfer(
        get_wcspr_key(),
        self.creator_graduation_reward_cspr,
        self.creator,
    );

    let pair_package_hash = add_dex_liquidity(
        self.token_key,
        self.cspr_reserve
            .checked_sub(self.creator_graduation_reward_cspr)
            .unwrap(),
        self.reserved_tokens_for_graduation,
    );

Code snippet of create function from contract/src/main.rs file:

if curve_exists.is_some() {
    runtime::revert(CustomError::CurveAlreadyCreated)
}

let mut curve = BondingCurve::create(
    token_contract_hash_key,
    new_token_initial_supply
        .checked_sub(reserved_tokens_for_graduation)
        .unwrap(),
    U256::zero(),
    reserved_tokens_for_graduation,
);

BVSS
Recommendation

It is recommended to add validation checks during contract initialization and in all setter functions to ensure creator_graduation_reward_cspr < cspr_reserves_graduation_threshold and reserved_tokens_for_graduation < new_token_initial_supply. These validations should revert the transaction if the logical constraints are violated, preventing the contract from entering an invalid state.

Remediation Comment

ACKNOWLEDGED: The Friendly Market team has acknowledged this finding.

7.11 Mismatch Between EntryPoint Type Signatures and Their Implementations

//

Informational

Description

The entry point definitions in contract/src/entry_points.rs and contract/src/cep18_entry_points.rs have severe mismatches with the actual function implementations:


CSPR.fun Contract Entry Points

set_is_paused

Incorrect type: is_paused (Key instead of bool)


create

Missing arguments:

  • name (String)

  • symbol (String)

  • description (String)

  • logo (String)

  • website (String)

  • twitter (String)

  • discord (String)

  • telegram (String)

  • additional_metadata (String)

  • tax_percentage_x100 (Option)

  • tax_recipient_key (Option)

Incorrect type: amount (Option instead of U256)


trade

Missing arguments:

  • token_to_trade_contract_hash_key (Key)

  • is_buy (bool)

  • min_amount_received (U256)

Incorrect type: recipient (Key instead of U256)


get_trade_output

Missing arguments:

  • token_initial_cspr_price (U256)

  • adjustment_factor_b (U256)

  • cspr_reserves (U256)

  • is_buy (bool)

Extra argument that shouldn't exist: recipient parameter


init

Missing arguments:

  • admin (Key)

  • protocol_fee_x100 (U256)

  • protocol_fee_recipient (Key)

  • cspr_reserves_graduation_threshold (U256)

  • new_token_initial_supply (U256)

  • token_initial_cspr_price (U256)

  • adjustment_factor_b (U256)

  • reserved_tokens_for_graduation (U256)

  • creator_graduation_reward_cspr (U256)

  • package_hash (Key)

  • dex_router_contract_hash_key (Key)

  • dex_router_contract_package_hash_key (Key)

  • dex_factory_key (Key)

  • wcspr_key (Key)


CEP18 Entry Points

get_tax_percentage

Incorrect return type: Returns u32 but entry point specifies U256


init

Missing arguments:

  • total_supply (U256)

  • cspr_fun_contract_package_hash_key (Key)


BVSS
Recommendation

It is recommended to resolve the discrepancies in each entry point's signature definition to align with the implementation.

Remediation Comment

SOLVED: The Friendly Market team solved this issue in the specified commit ID.

Remediation Hash

7.12 Unused Contract Installation Function

//

Informational

Description

The CEP18 contract contains an install_contract function that is not used within the codebase. This function appears to be legacy code from a standalone CEP18 implementation that was later integrated into the CSPR.fun platform.


However, CEP18 token creation and management is now handled entirely by the CSPR.fun contract through its own installation mechanism. The presence of this unused function adds unnecessary code bloat, increases the contract size, and creates confusion about the intended deployment architecture.

BVSS
Recommendation

It is recommended to remove the unused install_contract function from the CEP18 contract to reduce code bloat and eliminate potential confusion.

Remediation Comment

ACKNOWLEDGED: The Friendly Market team has acknowledged this finding.

7.13 Unnecessary Runtime Arguments in CEP18 Initialization

//

Informational

Description

In contract/src/cep18_utils.rs at lines 70-73, the CSPR.fun contract calls the init_cep18 function with unnecessary runtime arguments.


The init_cep18 function expects only the following arguments:

  • TOTAL_SUPPLY

  • PACKAGE_HASH

  • CSPR_FUN_CONTRACT_PACKAGE_HASH_KEY

  • TOKEN_METADATA

  • TAX_PERCENTAGE

  • TAX_RECIPIENT_KEY


However, the call includes two additional arguments that are not utilized:

  • CONTRACT_HASH_KEY — This argument is not processed by init_cep18

  • ARG_EVENTS_MODE — This argument is not processed by init_cep18


These extraneous arguments increase the overhead of the contract call without providing any functionality, as they are ignored by the receiving function.

Code Location

Below is a code snippet of the cep18_utils::create_cep18_token function:

let init_args = runtime_args! {ARG_TOTAL_SUPPLY => total_supply, PACKAGE_HASH => package_hash, CONTRACT_HASH_KEY => contract_hash_key, ARG_EVENTS_MODE => 1u8,
    CSPR_FUN_CONTRACT_PACKAGE_HASH_KEY=> get_this_contract_package_hash_as_key(),TOKEN_METADATA=> token_metadata, TAX_PERCENTAGE=> maybe_tax_percentage_x100.unwrap_or(0u32),TAX_RECIPIENT_KEY=> maybe_tax_recipient_key.unwrap_or(Key::Hash([1u8; 32]))
};
runtime::call_contract::<()>(contract_hash, INIT_CEP18_ENTRYPOINT_NAME, init_args);

BVSS
Recommendation

It is recommended to remove the unnecessary CONTRACT_HASH_KEY and EVENTS_MODE arguments from the runtime_args! macro call to clean up the contract interaction and reduce unnecessary data transmission.

Remediation Comment

ACKNOWLEDGED: The Friendly Market team has acknowledged this finding.

7.14 Unnecessary Type Conversion in Return Statement

//

Informational

Description

In contract/src/cep18_utils.rs, the create_cep18_token function performs an unnecessary type conversion in its return statement.


The function creates a contract_hash_key variable by converting contract_hash to Key type, but then in the return statement, it performs the same conversion again instead of reusing the already-converted contract_hash_key variable.


This redundant conversion adds unnecessary computational overhead and makes the code less efficient, as the same conversion operation is performed twice when it could be done once.

Code Location

Below is a code snippet of the create_cep18_token function:

let contract_hash_key = Key::from(contract_hash);
let package_hash = Key::from(contract_package_hash);
// let package_hash = Key::from(contract_package_hash);
let init_args = runtime_args! {ARG_TOTAL_SUPPLY => total_supply, PACKAGE_HASH => package_hash, CONTRACT_HASH_KEY => contract_hash_key, ARG_EVENTS_MODE => 1u8,
    CSPR_FUN_CONTRACT_PACKAGE_HASH_KEY=> get_this_contract_package_hash_as_key(),TOKEN_METADATA=> token_metadata, TAX_PERCENTAGE=> maybe_tax_percentage_x100.unwrap_or(0u32),TAX_RECIPIENT_KEY=> maybe_tax_recipient_key.unwrap_or(Key::Hash([1u8; 32]))
};
runtime::call_contract::<()>(contract_hash, INIT_CEP18_ENTRYPOINT_NAME, init_args);

(Key::from(contract_hash), package_hash)

BVSS
Recommendation

It is recommended to replace the redundant conversion in the return statement with the existing contract_hash_key variable.

Remediation Comment

ACKNOWLEDGED: The Friendly Market team has acknowledged this finding.

7.15 Unnecessary Wrapper Function Adds Code Bloat

//

Informational

Description

In contract/src/events.rs, the emit_ces_event function serves as an unnecessary wrapper around the ces function:


pub fn record_event_dictionary(event: Event) {
    emit_message(ARG_EVENTS, &event.to_json().into()).unwrap_or_revert();
    emit_ces_event(event);
}

pub fn emit_ces_event(event: Event) {
    ces(event);
}

``The emit_ces_event function is only called by record_event_dictionary and adds no value - it's simply a pass-through function that introduces unnecessary indirection and function call overhead.

BVSS
Recommendation

It is recommended to remove the emit_ces_event function and directly call ces(event) from record_event_dictionary:


pub fn record_event_dictionary(event: Event) {
    ces(event);
}

Remediation Comment

ACKNOWLEDGED: The Friendly Market team has acknowledged this finding.

7.16 Commented Out Code and Dead Code Indicates Code Quality Issues

//

Informational

Description

Multiple instances of commented out code and dead code exist throughout the codebase, indicating potential incomplete implementation or code quality issues:

  • The ENTRY_POINT_UPGRADE constant within the contract/src/cep18/constants.rs file is documented but not used anywhere in the codebase. This unused constant adds unnecessary complexity and may mislead developers about available functionality.

  • In contract/src/main.rs, there is commented out code for an allowance() entry point. This commented code suggests that the allowance functionality and CEP18 upgrade/installation functionality were intended to be implemented but were either abandoned or moved elsewhere, potentially indicating incomplete development or missing functionality.

  • In contract/src/cep18/lib.rs, there is commented out code for a call() entry point, which is not needed as the installation/upgrade logic is handled by the CSPR.fun contract.

  • In contract/src/cep18_utils.rs, there is a commented out code at Line 78: // let package_hash = Key::from(contract_package_hash); - This appears to be a duplicate declaration that was commented out.


The presence of multiple commented code blocks suggests either incomplete development, abandoned features, or lack of proper code cleanup during the development process.

BVSS
Recommendation

It is recommended to remove all commented out code and unused code to maintain a clean codebase. If any of the commented functionality is actually needed, it should be properly implemented rather than left as commented code.

Remediation Comment

ACKNOWLEDGED: The Friendly Market team has acknowledged this finding.

7.17 Documentation Inconsistencies

//

Informational

Description

The codebase contains multiple documentation inconsistencies that can mislead developers and create confusion during integration. In the CEP18 entry points file (contract/src/cep18_entry_points.rs), several functions have incorrect or outdated documentation. The get_tax_percentage function documentation incorrectly states it "Returns the set_lp_pair_key entry point" when it should describe returning the tax percentage entry point. Similarly, the change_security function documentation references parameters which are: burner_list and mint_and_burn_list.


The main contract entry points (contract/src/entry_points.rs) also contain misleading documentation. The create function documentation claims to return the trade entry point.


The main contract (contract/src/main.rs) contains documentation that references unimplemented features. The create function documentation mentions a launch_option parameter that doesn't exist in the implementation. While the documentation states this parameter determines whether to create tokens on a bonding curve or provide native tokens for DEX liquidity, the function actually always creates tokens on a bonding curve. This disconnect between documentation and implementation creates uncertainty about the contract's actual capabilities.


The contract/src/cep18/constants.rs file contains duplicate comments stating "Constants used by the CEP18 contract". This redundancy adds unnecessary code bloat and creates confusion about which comment is authoritative.

BVSS
Recommendation

It is recommended to:

1. Update all entry point documentation to accurately reflect current function implementations

2. Remove references to unimplemented parameters and features

3. Clarify actual functionality in function documentation

Remediation Comment

ACKNOWLEDGED: The Friendly Market team has acknowledged this finding.

7.18 Inconsistent Error Handling Patterns

//

Informational

Description

The CEP18 library contains inconsistent error handling patterns where some functions use specific error types while others use generic .unwrap_or_revert() calls:


get_tax_percentage Function (Line 278)

The get_tax_percentage function is the only getter function in the CEP18 contract that doesn't revert with Cep18Error::FailedToReturnEntryPointResult. Instead, it uses .unwrap_or_revert() which provides less specific error information.


Dictionary Creation in init_cep18 Function (Line 294)

Inside the init_cep18 function, the balances dictionary creation properly reverts with Cep18Error::FailedToCreateDictionary on failure. However, the allowances and security_badges dictionary creations use generic .unwrap_or_revert() instead of the specific error type, creating inconsistency in error handling patterns.


Bonding Curve Retrieval in get_by_token Function (Lines 93-100)

The get_by_token function uses generic .unwrap_or_revert() calls when a bonding curve doesn't exist for a given token. This provides no specific error information about why the operation failed, making it difficult to distinguish between a non-existent bonding curve and other potential failures. The function should revert with a specific error type like CustomError::BondingCurveNotFound to provide clear feedback about the exact cause of the failure.

BVSS
Recommendation

It is recommended to standardize error handling across the codebase by:

1. Updating get_tax_percentage function to use .unwrap_or_revert_with(Cep18Error::FailedToReturnEntryPointResult)

2. Updating allowances and security_badges dictionary creation to use Cep18Error::FailedToCreateDictionary

3) Updating the get_by_token function to use .unwrap_or_revert_with() with a specific error message indicating that the bonding curve doesn't exist for the given token.

Remediation Comment

ACKNOWLEDGED: The Friendly Market team has acknowledged this finding.

7.19 Debug Code Present in Production

//

Informational

Description

The contracts include multiple debug statements using runtime::print() that should be removed before deployment to production. These statements can introduce unnecessary overhead, increase the size of the contract, and potentially expose sensitive information in logs.

Code Location

Below are the locations of the debug statements:

contract/src/main.rs
391:    runtime::print("trade: got token key");

contract/src/cep18/lib.rs
290:    runtime::print("in init_cep18");
301:    runtime::print("cep18: before write_balance_to");
302:    runtime::print(&cspr_fun_contract_package_hash_key.to_formatted_string());
304:    runtime::print("cep18: after write_balance_to");

contract/src/bonding_curve.rs
188:    runtime::print("before getting amount_after_token_tax");
204:    runtime::print("after getting amount_after_token_tax");
212:    runtime::print("after getting protocol_fee");
214:    runtime::print("after getting amount_after_all_fees");
217:    runtime::print("after getting output");
235:    runtime::print("before transfers");

wrap-create-token-create-lp-session-code/src/main.rs
177:    runtime::print("in transfer");
194:    runtime::print("in transfer from");
684:    runtime::print("in add dex liquidity");
693:    runtime::print("before 2nd approve");
703:    runtime::print("after 2nd approve");
723:    runtime::print("after add liquidity router");
730:    runtime::print("before setting lp pair key");
739:    runtime::print("after setting lp pair key");

BVSS
Recommendation

It is recommended to remove all runtime::print() statements from production code.

Remediation Comment

SOLVED: The Friendly Market team solved this issue in the specified commit ID.

Remediation Hash

7.20 Inconsistent Naming Conventions For Fee and Tax Parameters

//

Informational

Description

The contract uses inconsistent naming conventions for protocol_fee and tax_percentage parameters across different modules. Specifically:


  1. Protocol fees use PROTOCOL_FEE_X100 consistently, while tax parameters use both TAX_PERCENTAGE_X100 (in main.rs) and TAX_PERCENTAGE (in cep18/constants.rs) for the same concept;

  2. Function names like get_cep18_tax_percentage() don't follow the same X100 suffix pattern as getprotocol_fee_x100();

  3. There's no clear documentation explaining that these values represent basis points (1/100th of a percent) and are divided by 10,000 in calculations.


This inconsistent naming can lead to implementation errors when developers assume different scaling conventions, potentially causing incorrect fee calculations or tax applications. While the current calculations are mathematically correct, the confusing nomenclature increases the risk of bugs in future modifications or integrations.

BVSS
Recommendation

It is recommended to standardize all fee and tax related naming conventions by either using BASISPOINTS suffix consistently (e.g., PROTOCOL_FEE_BASIS_POINTS, TAX_RATE_BASIS_POINTS) or _X100 suffix consistently throughout the codebase. Additionally, add clear documentation comments explaining the scaling convention and update function names to match the chosen convention.

Remediation Comment

ACKNOWLEDGED: The Friendly Market team has acknowledged this finding.

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

Friendly Casper Token Minter

* Use Google Chrome for best results

** Check "Background Graphics" in the print settings if needed

© Halborn 2025. All rights reserved.