Fan Fun - EVM Contracts - Fan Fun


Prepared by:

Halborn Logo

HALBORN

Last Updated 04/18/2025

Date of Engagement: February 26th, 2025 - February 28th, 2025

Summary

100% of all REPORTED Findings have been addressed

All findings

18

Critical

0

High

0

Medium

2

Low

5

Informational

11


1. Introduction

Fan.Fun engaged Halborn to conduct a security assessment of their Fandotfun project beginning on February 27th and ending on March 3rd. The security assessment was scoped to the smart contract provided in the GitHub repository. Commit hash and further details can be found in the Scope section of this report.


FanDotFun is a decentralized protocol for creator-fan engagement through time-decaying keys.

2. Assessment Summary

Halborn was provided 3 days for the engagement and assigned one full-time security engineer to review the security of the smart contract in scope. The engineer is a blockchain and smart contract security expert with advanced penetration testing and smart contract hacking skills, and deep knowledge of multiple blockchain protocols.

The purpose of the assessment is to:

    • Identify potential security issues within the smart contract.

    • Ensure that smart contract functionality operates as intended.


In summary, Halborn identified some improvements to reduce the likelihood and impact of risks, which were partially addressed by the Fan.Fun team. The main ones were the following: 

    • Implement Slippage Protection and/or return excess funds sent by subscribers.

    • Consider adopting a pull pattern for claiming decayed keys.

    • Review fee calculations in claim() or prevent protocol fees from being 0.


3. Test Approach and Methodology

Halborn performed a combination of manual and automated security testing to balance efficiency, timeliness, practicality, and accuracy in regard to the scope of this assessment. While manual testing is recommended to uncover flaws in logic, process, and implementation; automated testing techniques help enhance coverage of the code and can quickly identify items that do not follow the security best practices. The following phases and associated tools were used during the assessment:

    • Research into architecture and purpose.

    • Smart contract manual code review and walkthrough.

    • Graphing out functionality and contract logic/connectivity/functions (solgraph).

    • Manual assessment of use and safety for the critical Solidity variables and functions in scope to identify any arithmetic related vulnerability classes.

    • Manual testing by custom scripts.

    • Static Analysis of security for scoped contract, and imported functions (slither).


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: contracts
(b) Assessed Commit ID: 5281e99
(c) Items in scope:
  • src/IKeys.sol
  • src/Keys.sol
  • src/KeysLibrary.sol
Out-of-Scope: Third party dependencies and economic attacks.
Files and Repository
(a) Repository: contracts
(b) Assessed Commit ID: 3a72d60
(c) Items in scope:
  • src/IKeys.sol
  • src/Keys.sol
  • src/KeysLibrary.sol
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

0

Medium

2

Low

5

Informational

11

Security analysisRisk levelRemediation Date
Front-Running Risks: Potential DoS, Sandwich Attacks, and Slippage in Buy/SellMediumSolved - 03/06/2025
Creators Could Prevent Their Subscribers From ClaimingMediumRisk Accepted - 03/10/2025
Protocol Cannot Have 0% Fees Due to Adjusted Calculation in Claim()LowRisk Accepted - 03/10/2025
Malicious Subscribers Could Potentially DoS ClaimsLowRisk Accepted - 03/10/2025
Missing Emergency Withdrawal CapabilityLowSolved - 03/06/2025
Missing Pausing MechanismLowRisk Accepted - 03/10/2025
Centralization RisksLowRisk Accepted - 03/10/2025
Subscribers Can Buy Zero KeysInformationalAcknowledged - 03/10/2025
Single-step Ownership Transfer ProcessInformationalAcknowledged - 03/10/2025
Owner Can Renounce OwnershipInformationalAcknowledged - 03/10/2025
Missing Visibility AttributeInformationalSolved - 03/07/2025
Lack of Event EmissionInformationalSolved - 03/06/2025
Magic Numbers in UseInformationalSolved - 03/07/2025
Style Guide OptimizationsInformationalSolved - 03/07/2025
Events Missing Indexed FieldsInformationalAcknowledged - 03/10/2025
Incomplete NatSpec DocumentationInformationalSolved - 03/07/2025
Missing Input ValidationInformationalAcknowledged - 03/10/2025
Inconsistent License DeclarationsInformationalSolved - 03/07/2025

7. Findings & Tech Details

7.1 Front-Running Risks: Potential DoS, Sandwich Attacks, and Slippage in Buy/Sell

//

Medium

Description

The current implementation of the Keys contract enforces users to submit buying transactions with an exact amount of native tokens, as any excess funds are not refunded. The current design exposes the buyKeys() and sellKeys() functions to front-running attacks. An attacker could monitor pending transactions and then submit their own transaction with the exact required value before a victim's transaction is mined, thereby altering the key supply and price. As a result, the victim’s transaction may revert, effectively leading to a denial-of-service (DoS) scenario on key purchases.


Furthermore, a similar front-running strategy on the sell side could allow an attacker to secure a more favorable selling price or exploit the timing around key decay, causing unexpected slippage for legitimate sellers. These vulnerabilities may also facilitate sandwich attacks, enabling attackers to profit from the price differences created between front-run and victim transactions.


The risk of this finding was increased due to the lack of an emergency withdraw function as explained in the "Missing Emergency Withdrawal Capability" finding.

BVSS
Recommendation
  • Implement Slippage Protection: Allow users to specify a minimum acceptable output for buy and sell transactions. This mechanism would ensure that transactions revert if the execution price deviates beyond a preset threshold, thereby reducing the impact of front-running.

  • Enable Refunds for Excess Funds: Modify the contract to refund any overpayment. This change would let users include a buffer amount in their transaction, decreasing the need to send the exact amount and lowering the risk of DoS via front-running.

  • Enhance Monitoring and Configurability: Consider adding configurable parameters for slippage limits and closely monitor transaction activity. These adjustments would provide additional safeguards against front-running and sandwich attacks as market activity increases.

Implementing these measures will help mitigate the risk of attackers manipulating transaction ordering, thereby enhancing the resilience of the contract’s key buy and sell flows.

Remediation Comment

SOLVED: The Fan.Fun team fixed this finding in commit 2a15092 by implementing a refund for excess payment in buyKeys() and accepting the risks of the scenarios where users front-run buying keys. The team indicated:

"We believe DOS via buying keys is not really a DOS attack. If someone is front-running by buying keys, they are participating in the protocol as intended.

Slippage protection could make things worse since they can enable sandwich attacks. Attackers could manipulate price within the slippage range. This would be worse because users would use actual value rather than just having transactions revert.

Since we are deploying on a fast chain, users can quickly retry with an updated price. We expect transaction costs to be low to the point where the user will not mind a few cents spent on a failed transaction and will prefer this over getting sandwiched.

With the current design: if the price moves unfavorably, the transaction reverts. Users can then decide whether to buy at the new price.

We do acknowledge that if claim isn't called in time to adjust balances, a subscriber will be able to sell at a higher price than they deserve. Because of block gas limits, we are limited in how many subscribers we can process in a single call. Therefore, we thought the best mechanism is to regularly call claims to correct the price. The advantage is temporary and bounded by how often claims are processed.

We implemented an auto refund if users pay more than they should".

Remediation Hash

7.2 Creators Could Prevent Their Subscribers From Claiming

//

Medium

Description

In the claim() function, the contract transfers the creator's portion of decayed value using the following line:

(bool creatorTransferSuccess,) = creator.call{value: creatorAmount}("");

A creator using a smart contract wallet could upgrade or modify its implementation to intentionally revert any incoming native funds. This would cause the above call to always fail, forcing the entire claim transaction to revert and preventing all subscribers from receiving their claims. This risk is further amplified by the absence of an emergency withdrawal function in the Keys contract, leaving any stuck funds unrecoverable.


BVSS
Recommendation

Consider implementing some of the following protections:

  • Implement Fallback Mechanisms: Consider adding logic that allows subscribers or an authorized party (e.g., the contract owner) to redirect the creator’s share to a predefined fallback address if the transfer fails repeatedly.

  • Adopt Pull Over Push Patterns: Transition from a push-based fund transfer approach to a pull-based mechanism (see Pull Over Push Pattern). This would let creators withdraw their share at their convenience, preventing a malicious creator's wallet from blocking the claim process.

  • Integrate Emergency Withdrawal: Incorporate an emergency withdrawal or sweep function to recover native funds in cases where funds become irretrievably locked due to creator misbehavior.

  • Reassess Creator Fund Transfers: Reevaluate the design of fund transfers in the claim function to decouple state updates from external calls, ensuring that a single malicious creator cannot block the entire claim process.

These measures would reduce the risk of a malicious creator obstructing the claim process and improve the overall resilience and reliability of the contract's fund management.

Remediation Comment

RISK ACCEPTED: The Fan.Fun team accepted the risk of this finding indicating "To handle the issue of a creator potentially blocking claims, we will ignore calling claim on this creator. We will monitor this off chain and blacklist creators who do this".

7.3 Protocol Cannot Have 0% Fees Due to Adjusted Calculation in Claim()

//

Low

Description

If the decay protocol fee percentage (decayProtocolFeePercent) is set to 0, the claim process will revert. This is due to the adjusted fee calculation in the claim function. In the code snippet below, if decayProtocolFeePercent is 0, then protocolFeeAmount becomes 0. However, the subsequent subtraction (protocolFeeAmount - 1) will underflow, causing the transaction to revert:


function claim(address creator, address[] calldata subscribers) public {
  ...
  // Calculate fees using decay-specific fee percentages (using basis points)
  uint256 protocolFeeAmount = (totalDecayedValue * decayProtocolFeePercent) / 10_000;
  uint256 subscriberFeeAmount = (totalDecayedValue * decaySubscriberFeePercent) / 10_000;
  uint256 creatorAmount = totalDecayedValue - protocolFeeAmount - subscriberFeeAmount;

  uint256 adjustedProtocolFee = protocolFeeAmount - 1;

BVSS
Recommendation
  • Enforce a Minimum Fee: Add input validation in the constructor and/or setter functions to ensure that decayProtocolFeePercent cannot be set to 0.

  • Conditional Adjustment: Alternatively, modify the fee calculation logic to only subtract 1 when protocolFeeAmount is greater than 0. This will allow the fee to be 0 without causing an underflow.

  • Review Fee Adjustment Logic: Revisit the rationale behind subtracting 1 from protocolFeeAmount to ensure it meets the intended economic model without introducing risks when fee percentages are low or zero.

Implementing one of these approaches will prevent unintended reverts during the claim process and ensure the contract behaves as expected even when a fee of 0% might be desired.

Remediation Comment

RISK ACCEPTED: The Fan.Fun team accepted the risk of this finding indicating "We acknowledge this issue. We will never set decayProtocolFeePercent to 0".

7.4 Malicious Subscribers Could Potentially DoS Claims

//

Low

Description

The claim() function distributes subscriber fees by iterating over a caller-supplied list of subscriber addresses and sending funds using a low-level call:

for (uint256 i = 0; i < subscribers.length; i++) {
  if (subscriberRemainingKeys[i] > 0) {
    uint256 subscriberFee = (subscriberFeeAmount * subscriberRemainingKeys[i]) / totalRemainingKeys;
    (bool subscriberTransferSuccess,) = subscribers[i].call{value: subscriberFee}("");

Because the caller provides the subscriber list, a malicious subscriber can potentially be excluded from the claim. This mitigates the risk if the caller carefully curates the list. However, if the list is compiled automatically or without thorough vetting, an address that always reverts on receiving ETH can cause the entire claim transaction to revert, leading to a denial-of-service (DoS) for the claim process.

BVSS
Recommendation

While the current design allows the caller to exclude known malicious subscribers from the list, this relies on manual vigilance and may not be foolproof in automated systems.

To further mitigate this risk, it is recommended to adopt a pull over a push pattern for external fund transfers. Specifically, instead of transferring subscriber fees directly in the claim process, record the owed amounts in a mapping (e.g., pendingWithdrawals) and let subscribers withdraw their funds via a separate withdraw() function. This approach minimizes the impact of any single malicious subscriber and enhances overall contract resilience against DoS attacks.

Remediation Comment

RISK ACCEPTED: The Fan.Fun team accepted the risk of this finding indicating "To handle the issue of a subscriber potentially blocking claims, we will ignore calling claim on this subscriber. We will monitor this off chain and blacklist subscribers who do this. We are highly optimizing for gas savings, therefore we are opting for the push pattern instead of the pull pattern where we have to store amounts per user".

7.5 Missing Emergency Withdrawal Capability

//

Low

Description

The Keys contract currently lacks an explicit withdraw or sweep function to recover native funds that become stuck in the contract. This design decision can lead to situations where excess funds remain trapped.


For example, if a user checks the price at X amount and then submits a buy order with X (even though the price drops to X-1 before execution) the extra funds are not refunded. This behavior creates potential financial inefficiencies and user dissatisfaction. This issue is related to the previously noted "Front-Running Risks: Potential DoS, Sandwich Attacks, and Slippage in Buy/Sell" finding, where the requirement for exact payment further exacerbates the risk of funds becoming stuck.

BVSS
Recommendation
  • Implement Refund Logic: Update the buy (and possibly sell) functions to automatically refund any excess funds if the executed price is lower than the amount sent.

  • Consider Emergency Withdrawal Options: Although the primary design discourages a sweep function, evaluate the possibility of an optional, secure emergency withdrawal mechanism that allows users to recover funds accidentally sent or left over due to price fluctuations.

  • Update Documentation: Clearly document any changes made to the refund mechanism and/or emergency withdrawal process so that users understand how to recover excess funds.

These modifications will help mitigate the risk of funds being irretrievably locked in the contract, improving overall user experience and aligning with best practices for handling native tokens.

Remediation Comment

SOLVED: The Fan.Fun team fixed this finding in commit 2a15092 by implementing a refund for excess payment in buyKeys() so funds are not stuck.

Remediation Hash

7.6 Missing Pausing Mechanism

//

Low

Description

The Keys contract does not include a pause mechanism that would allow the contract owner to temporarily disable certain functions in the contract. A pause mechanism is a critical feature that can be used to prevent potential issues in the event of a bug or vulnerability being discovered in the contract. It can also be used to prevent malicious actors from exploiting the contract while a fix is being implemented.

BVSS
Recommendation

Consider implementing a pause mechanism in the Keys contract to allow the contract owner to disable certain functions in the contract in case of an emergency or bug discovery.

Remediation Comment

RISK ACCEPTED: The Fan.Fun team accepted the risk of this finding indicating "We don't foresee a need to pause our system. We don't want the ability to pause our system".

7.7 Centralization Risks

//

Low

Description

The protocol implements an extensive privileged role with significant control over critical protocol parameters and functionality. The owner role in this contract can execute powerful administrative functions that could affect the entire protocol's operation and user funds.


function setFeeDestination(address _feeDestination) external onlyOwner {
function setProtocolFeePercent(uint256 _feePercent) external onlyOwner {
function setSubscriberFeePercent(uint256 _feePercent) external onlyOwner {
function setDecayProtocolFeePercent(uint256 _feePercent) external onlyOwner {
function setDecaySubscriberFeePercent(uint256 _feePercent) external onlyOwner {

Related notes:

  • If the owner sets the protocolFeeDestination to an address that does not accept native funds, the buyKeys() for the whole protocol would fail.

  • The design choice that creators can buy their own keys before unlocking buys could also be considered a centralization for creators.


BVSS
Recommendation

It is recommended the following:

  1. Adopt Multisig or DAO-Based Governance: Transition the owner role to a multi-signature wallet or a DAO/governance model to distribute control and reduce reliance on a single party.

  2. Enhance Off-Chain Signer Security:

    • Use an institutional-grade Hardware Security Module (HSM) or a threshold-signature scheme (e.g., MPC) for the off-chain signer key.

    • Implement robust key management policies and regular key rotations to reduce the risk of permanent compromise.

  3. On-Chain Validation of Critical Operations: Where feasible, shift critical or sensitive logic on-chain to reduce reliance on off-chain logic.

  4. Emergency Procedures and Transparency:

    • Provide transparent documentation or a public dashboard to inform users of any centralized intervention (e.g., pausing the protocol).

    • Plan a contingency strategy (e.g., timelock for changes or a publicly broadcasted freeze period) to mitigate abrupt or unannounced governance decisions.


By distributing control and securing off-chain components, the protocol can significantly lower its centralization risk and foster greater trust among participants.

Remediation Comment

RISK ACCEPTED: The Fan.Fun team accepted the risk of this finding indicating "We accept the risks. We will follow best practices and use a multisig owner in production".

7.8 Subscribers Can Buy Zero Keys

//

Informational

Description

While it was verified that it is not possible to sell an amount of 0 keys, it was found that users can buy 0 keys and effectively update the lastPurchaseTimestamp value.

Proof of Concept

Run the following test function to verify that calling buyKeys() with an amount of 0 updates the lastPurchaseTimestamp of the subscriber.

function test_Hal_buy_ZeroAmount() public {
    // Allocate 100 ether to subscriber1
    vm.deal(subscriber1.addr, 100 ether);

    // Advance time by 1 day to ensure currentPeriod > 0 before the subscriber buys keys
    vm.warp(block.timestamp + 1 days);

    // Start acting as subscriber1
    vm.startPrank(subscriber1.addr);

    // Calculate the price to buy 0 keys using the same method as the contract
    (uint256 basePrice, uint256 protocolFee) = keys.getBuyPriceAfterFee(creator1.addr, 0);

    c.log("Initial totalSupplies: %s", keys.totalSupplies(creator1.addr));
    (uint256 balance, uint256 lastPurchaseTimestamp) = keys.subscriberInfo(creator1.addr, subscriber1.addr);
    c.log("Initial subscriber1 balance: %s and timestamp %s", balance, lastPurchaseTimestamp);

    // Buy 0 keys for creator1
    keys.buyKeys{value: basePrice + protocolFee}(creator1.addr, 0);

    c.log("After totalSupplies: %s", keys.totalSupplies(creator1.addr));
    (balance, lastPurchaseTimestamp) = keys.subscriberInfo(creator1.addr, subscriber1.addr);
    c.log("After subscriber1 balance: %s and timestamp %s", balance, lastPurchaseTimestamp);

    vm.stopPrank();
}

BVSS
Recommendation

Consider adding a zero amount validation check in the buyKeys() function as found in sellKeys().

Remediation Comment

ACKNOWLEDGED: The Fan.Fun team acknowledged the risk of this finding indicating "The user can buy 0 keys, but it cannot be used to manipulate expiry times. Therefore, no change is needed".

7.9 Single-step Ownership Transfer Process

//

Informational

Description

It was identified that the Keys contract inherits from OpenZeppelin's Ownable library. Ownership of the contracts that are inherited from the Ownable module can be lost, as the ownership is transferred in a single-step process.


The address that the ownership is changed to should be verified to be active or willing to act as the owner. Ownable2Step is safer than Ownable for smart contracts because the owner cannot accidentally transfer smart contract ownership to a mistyped address. Rather than directly transferring to the new owner, the transfer only completes when the new owner accepts ownership.

BVSS
Recommendation

To mitigate the risks associated with single-step ownership transitions and enhance contract security, it is recommended to adopt a two-step ownership transition mechanism, such as OpenZeppelin's Ownable2Step. This approach introduces an additional step in the ownership transfer process, requiring the new owner to accept ownership before the transition is finalized. The process typically involves the current owner calling a function to nominate a new owner, and the nominee then calling another function to accept ownership.


Implementing Ownable2Step provides several benefits:

1. Reduces Risk of Accidental Loss of Ownership: By requiring explicit acceptance of ownership, the risk of accidentally transferring ownership to an incorrect or zero address is significantly reduced.

2. Enhanced Security: It adds another layer of security by ensuring that the new owner is prepared and willing to take over the responsibilities associated with contract ownership.

3. Flexibility in Ownership Transitions: Allows for a smoother transition of ownership, as the nominee has the opportunity to prepare for the acceptance of their new role.


By adopting Ownable2Step, contract administrators can ensure a more secure and controlled process for transferring ownership, safeguarding against the risks associated with accidental or unauthorized ownership changes.

Remediation Comment

ACKNOWLEDGED: The Fan.Fun team acknowledged the risk of this finding indicating "We are okay with the risks here".

7.10 Owner Can Renounce Ownership

//

Informational

Description

It was identified that the Keys contract inherits from OpenZeppelin's Ownable library. In the Ownable contracts, the renounceOwnership() function is used to renounce the Owner permission. Renouncing ownership before transferring would result in the contract having no owner, eliminating the ability to call privileged functions.

/**
 * @dev Leaves the contract without owner. It will not be possible to call
 * `onlyOwner` functions. Can only be called by the current owner.
 *
 * NOTE: Renouncing ownership will leave the contract without an owner,
 * thereby disabling any functionality that is only available to the owner.
 */
function renounceOwnership() public virtual onlyOwner {
    _transferOwnership(address(0));
}

BVSS
Recommendation

It is recommended that the Owner cannot call renounceOwnership() without first transferring ownership to another address. In addition, if a multi-signature wallet is used, the call to the renounceOwnership() function should be confirmed for two or more users.

Remediation Comment

ACKNOWLEDGED: The Fan.Fun team acknowledged the risk of this finding indicating "We are okay with the risks here".

7.11 Missing Visibility Attribute

//

Informational

Description

It is best practice to set the visibility of state variables and constants explicitly. In the KeysLibrary library, three constants did not have the visibility set:

uint256 constant A = 2_000_000_000_000_000_000; // 2.0 in fixed point
uint256 constant B = 150_000; // Multiplier
uint256 constant C = 1_000_000_000_000_000; // Base price

BVSS
Recommendation

Consider explicitly setting the visibility of all state variables and constants. More specifically, the code could be updated to:

uint256 internal constant _A = 2_000_000_000_000_000_000; // 2.0 in fixed point
uint256 internal constant _B = 150_000; // Multiplier
uint256 internal constant _C = 1_000_000_000_000_000; // Base price

Remediation Comment

SOLVED: The Fan.Fun team fixed this finding in commit 5c65c25 by setting the visibility of all constants as recommended.

Remediation Hash

7.12 Lack of Event Emission

//

Informational

Description

It has been observed that some functionalities are missing emitting events. Events are a method of informing the transaction initiator about the actions taken by the called function. It logs its emitted parameters in a specific log history, which can be accessed outside of the contract using some filter parameters. Events help non-contract tools to track changes, and events prevent users from being surprised by changes.


Instances in Keys.sol:

function setFeeDestination(address _feeDestination) external onlyOwner {
    protocolFeeDestination = _feeDestination;
}

function setProtocolFeePercent(uint256 _feePercent) external onlyOwner {
    protocolFeePercent = _feePercent;
}

function setSubscriberFeePercent(uint256 _feePercent) external onlyOwner {
    subscriberFeePercent = _feePercent;
}

function setDecayProtocolFeePercent(uint256 _feePercent) external onlyOwner {
    decayProtocolFeePercent = _feePercent;
}

function setDecaySubscriberFeePercent(uint256 _feePercent) external onlyOwner {
    decaySubscriberFeePercent = _feePercent;
}

This list is not exhaustive, and it is recommended to review the entire codebase to identify additional instances where event emissions may be beneficial. A thorough analysis should be conducted to determine whether adding event emissions aligns with the intended design and provides value in tracking state changes.

BVSS
Recommendation

All functions updating important parameters should emit events.

Remediation Comment

SOLVED: The Fan.Fun team fixed this finding in commit aa06bf6 by adding event emissions to all setters as recommended.

Remediation Hash

7.13 Magic Numbers in Use

//

Informational

Description

In programming, "magic numbers" refer to the use of hard-coded numerical or string values directly in the code without clear indication of their purpose or origin. The use of magic numbers can lead to confusion and make the codebase more difficult to understand, maintain, and update.


Several instances of magic numbers were found in the provided contracts. These values lack meaningful context, making it unclear whether they are arbitrary, derived from a specific standard, or require future modification. Their presence increases the likelihood of errors during maintenance or updates.


  • In Keys.sol:

decayProtocolFeePercent = 1000; // 10%
decaySubscriberFeePercent = 1000; // 10%
...
uint256 protocolFeeAmount = (totalDecayedValue * decayProtocolFeePercent) / 10_000;
uint256 subscriberFeeAmount = (totalDecayedValue * decaySubscriberFeePercent) / 10_000;
...
protocolFee = (basePrice * protocolFeePercent) / 10_000;

  • In KeysLibrary.sol:

UD60x18 supplyFP = ud(supply * 1e18);
UD60x18 newSupplyFP = ud((supply + amount) * 1e18);
...
uint256 denominator = (A * B) / 1e18;
...
uint256 secondTerm = (C * amount) / 1e18;

This list is not exhaustive, and it is recommended to review the entire codebase to identify additional instances where magic numbers are used.

BVSS
Recommendation

To improve code maintainability, readability, and reduce the risk of potential errors, it is recommended to replace magic numbers with well-defined constants. By using constants, developers can provide clear and descriptive names for specific values, making the code easier to understand and maintain. Additionally, updating the values becomes more straightforward, as changes can be made in a single location, reducing the risk of errors and inconsistencies. For large numbers, consider using scientific notation (e.g., 1e4).

Remediation Comment

SOLVED: The Fan.Fun team fixed this finding in commit 7c9a86b and 9beca85 by removing magic numbers from the code. Note: it is worth mentioning that scientific notations would further increase readability.

Remediation Hash

7.14 Style Guide Optimizations

//

Informational

Description

The project codebase contains several stylistic inconsistencies and deviations from Solidity best practices, which, while not directly impacting functionality, reduce code readability, maintainability, and adherence to standard conventions. Addressing these inconsistencies can enhance the clarity and professionalism of the code.


Examples:

  • Constants not in UPPER_CASE: The periodDuration constant below is not in UPPER_CASE as recommended by the Solidity style guide.

uint256 public constant periodDuration = 1 days;
  • Local State Variable Could Be A Constant: The denominator local state variable of the getTotalCost() function in KeysLibrary is always 300000, but it is declared as a calculation of three different constant values:

uint256 denominator = (A * B) / 1e18;

BVSS
Recommendation

Consider implementing the style improvements highlighted above to enhance the readability and consistency of the project.

Remediation Comment

SOLVED: The Fan.Fun team fixed this finding in commit 6e55670 by improving the style as recommended.

Remediation Hash

7.15 Events Missing Indexed Fields

//

Informational

Description

Indexed event fields make the data more quickly accessible to off-chain tools that parse events, and add them to a special data structure known as "topics" instead of the data part of the log. A topic can only hold a single word (32 bytes) so if you use a reference type for an indexed argument, the Keccak-256 hash of the value is stored as a topic instead.


Each event can use up to three indexed fields. If there are fewer than three fields, all the fields can be indexed. It is important to note that each index field costs extra gas during emission, so it's not necessarily best to index the maximum allowed fields per event (three indexed fields).


This is specially recommended when gas usage is not particularly of concern for the emission of the events in question, and the benefits of querying those fields in an easier and straight-forward manner surpasses the downsides of gas usage increase.

BVSS
Recommendation

Consider adding the indexed keyword when declaring events, considering the following example in IKeys.sol:

event Claimed(address indexed creator, uint256 keyAmount, uint256 baseAmount, uint256 protocolFeeAmount);

event Subscription(
  address indexed subscriber,
  address indexed creator,
  bool isSubscribe,
  uint256 keyAmount,
  uint256 hypeAmount,
  uint256 protocolHypeAmount,
  uint256 supply
);

Remediation Comment

ACKNOWLEDGED: The Fan.Fun team acknowledged this finding indicating "We have an indexer that is already storing and indexing all event data in its own database, regardless of whether the fields are marked as indexed. Therefore, in our case we don't see a benefit in using the indexed keyword. We have removed the indexed keyword on all events to save gas.". The indexed keyboards were removed in commit bdeda88.

Remediation Hash

7.16 Incomplete NatSpec Documentation

//

Informational

Description

Although much of the code under review adheres to a generally consistent NatSpec standard, there are notable gaps and inconsistencies in the documentation. For example, some functions in IKeys.sol lack any NatSpec annotations entirely, and there is inconsistent spacing (e.g., missing new lines between error declarations and subsequent NatSpec comments).

These documentation shortcomings can hinder a clear understanding of the contract logic, making it more challenging for external developers, users, and auditors to fully grasp the intended behavior, usage constraints, and potential edge cases.

BVSS
Recommendation

Adopt a unified NatSpec documentation approach throughout the entire codebase. Specifically:

  1. Function-Level Annotations: Provide full NatSpec annotations (@notice, @dev, @param, @return, etc.) for every function, detailing its purpose, parameter usage, and return data.

  2. File-Level Headers: Add standardized titles and author tags at the beginning of all contract files, including helper and core contracts, to improve clarity and maintain consistency.

  3. Comprehensive Coverage: Extend NatSpec documentation to cover all structs, errors, and interfaces. In particular, address missing annotations in files like IKeys.sol and ensure proper formatting (such as maintaining appropriate new lines between declarations) for better readability.

Implementing these recommendations will significantly enhance the clarity, maintainability, and auditability of the codebase.

Remediation Comment

SOLVED: The Fan.Fun team fixed this finding in commits 710372e and c7ed3a8 by improving the NatSpec documentation as recommended.

Remediation Hash

7.17 Missing Input Validation

//

Informational

Description

During the security assessment, it was identified that some functions in the smart contracts lack proper input validation, allowing critical parameters to be set to undesired or unrealistic values. This can lead to potential vulnerabilities, unexpected behavior, or erroneous states within the contract. Examples include:


The constructor in Keys.sol does not enforce that _protocolFeeDestination is non-zero, nor does it validate that _protocolFeePercent and _subscriberFeePercent fall within acceptable ranges:

constructor(address _protocolFeeDestination, uint256 _protocolFeePercent, uint256 _subscriberFeePercent)
  Ownable(msg.sender)
{
  protocolFeeDestination = _protocolFeeDestination;
  protocolFeePercent = _protocolFeePercent;
  subscriberFeePercent = _subscriberFeePercent;
...

Furthermore, the setters were also missing validations:

function setFeeDestination(address _feeDestination) external onlyOwner {
    protocolFeeDestination = _feeDestination;
}

function setProtocolFeePercent(uint256 _feePercent) external onlyOwner {
    protocolFeePercent = _feePercent;
}

function setSubscriberFeePercent(uint256 _feePercent) external onlyOwner {
    subscriberFeePercent = _feePercent;
}

function setDecayProtocolFeePercent(uint256 _feePercent) external onlyOwner {
    decayProtocolFeePercent = _feePercent;
}

function setDecaySubscriberFeePercent(uint256 _feePercent) external onlyOwner {
    decaySubscriberFeePercent = _feePercent;
}

This list is not exhaustive. It is recommended to conduct a comprehensive review of the codebase to identify and assess other functions that may require additional input validation. Ensuring appropriate checks are in place for critical parameters will enhance the overall reliability, security, and predictability of the contracts.

BVSS
Recommendation

To mitigate these issues, implement input validation in all constructor functions and other critical functions to ensure that inputs meet expected criteria. This can prevent unexpected behaviors and potential vulnerabilities.

Remediation Comment

ACKNOWLEDGED: The Fan.Fun team acknowledged the risk of this finding indicating "We are okay with the risks here. We have tests in place to avoid deployment with incorrect parameters".

7.18 Inconsistent License Declarations

//

Informational

Description

The project demonstrates inconsistent usage of SPDX license declarations across its files. Specifically, the following licenses are specified in different parts of the codebase:

  • // SPDX-License-Identifier: MIT

  • // SPDX-License-Identifier: BUSL


These variations in licensing can lead to potential compatibility and compliance issues, particularly when combining or redistributing code.

BVSS
Recommendation
  • Standardize Licensing: Review the project’s intended licensing model and ensure all files use the same SPDX license declaration to align with that model.

  • Segregate Code with Different Licenses: If certain parts of the project require different licenses (e.g., open-source vs proprietary), clearly separate these components into distinct modules or repositories to avoid conflicts.

  • Seek Legal Advice: Consult legal experts to verify that the chosen licenses comply with your project’s objectives and that all licensing terms are properly adhered to, minimizing legal exposure.

  • Document Licensing Strategy: Provide a clear licensing overview in the project’s documentation or repository (e.g., a LICENSE file or README) to ensure contributors and users understand the licensing terms and requirements.


Remediation Comment

SOLVED: The Fan.Fun team fixed this finding in commit 6e5ad34 by keeping the interface (IKeys.sol) as MIT to allow anyone to use it without any problem, but using a 1 year BUSL license for Keys.sol. Fan.Fun also added the related license files.

Remediation Hash

8. Automated Testing

Static Analysis Report

Description

Halborn used automated testing techniques to enhance the coverage of certain areas of the smart contracts in scope. Among the tools used was Slither, a Solidity static analysis framework. After Halborn verified the smart contracts in the repository and was able to compile them correctly into their abis and binary format, Slither was run against the contracts. This tool can statically verify mathematical relationships between Solidity variables to detect invalid or inconsistent usage of the contracts' APIs across the entire code-base.


All issues identified by Slither were proved to be false positives or have been added to the issue list in this report.


Output





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.