DexLyn Disclosed Report

Insufficient precision in `reward_increment` calculation allows reward exploitation by bad actors

Company
Created date
Nov 05 2025

Target

https://github.com/hackenproof-public/tokenomics_contract

Vulnerability Details

In the reward_per_token_internal() function, the protocol calculates the reward_increment as follows:

let reward_increment =
    ((time_diff as u256) * (gauge.reward_rate) * (DXLYN_DECIMAL as u256))
        / ((gauge.total_supply as u256) * PRECISION);
    let last_time_reward_applicable =
                math64::min(timestamp::now_seconds(), gauge.period_finish);

            // Calculate the time difference since the last update
            let time_diff = last_time_reward_applicable - gauge.last_update_time;

            // Compute reward increment with scaled reward_rate
            // Convert to u256 for precision loss prevention and handel overflow issue
            let reward_increment =
                ((time_diff as u256) * (gauge.reward_rate) * (DXLYN_DECIMAL as u256))
                    / ((gauge.total_supply as u256) * PRECISION);

            gauge.reward_per_token_stored + reward_increment


Where:

  • DXLYN_DECIMAL is set to 100,000,000.
      /// 1 DXLYN_DECIMAL in smallest unit (10^8), for token amount scaling
      const DXLYN_DECIMAL: u64 = 100_000_000;
    
    
  • reward_rate is calculated by reward / gauge.duration, where the reward is distributed based on the user's voting share in the voter contract.
        // Scaled reward to extra 10^4 to avoid precision issues in reward rate calculations.
        let reward = (reward as u256) * PRECISION;

        //if time more then finish period then calculate new reward rate other wise remaining
        // This logic is still loose some precision
        if (current_time >= gauge.period_finish) {
            gauge.reward_rate = reward / (gauge.duration as u256);
        } else {
            let remaining = (gauge.period_finish - current_time as u256);
            let left_over = remaining * gauge.reward_rate;
            gauge.reward_rate = (reward + left_over) / (gauge.duration as u256);
        };


  • time_diff is the time difference between the current time and the last update time.

    let last_time_reward_applicable =
                  math64::min(timestamp::now_seconds(), gauge.period_finish);
    
              // Calculate the time difference since the last update
              let time_diff = last_time_reward_applicable - gauge.last_update_time;
    
    
  • PRECISION and reward_rate have a precision of 10^4, while DXLYN_DECIMAL and total_supply have a precision of 10^8.

If time_diff is exactly 1 second and total_supply is large, the resulting reward_increment can be calculated as zero due to precision loss. A malicious attacker can exploit this by depositing a very small amount of tokens and repeatedly calling get_reward() every second to update reward_per_token_stored and last_update_time. This results in reward_per_token_stored remaining unchanged, while last_update_time keeps increasing, ultimately causing all users to lose their rewards.

    fun update_reward(gauge: &mut GaugeClmm, account: address) {
        gauge.reward_per_token_stored = reward_per_token_internal(gauge);
        gauge.last_update_time = math64::min(
            timestamp::now_seconds(), gauge.period_finish
        );

        if (account != @0x0) {
            let earned = earned_internal(gauge, account);
            table::upsert(&mut gauge.rewards, account, earned);

            table::upsert(
                &mut gauge.user_reward_per_token_paid,
                account,
                gauge.reward_per_token_stored
            );
        }
    }

To prevent this issue, change the multiplication factor for reward_increment to 2^64 to avoid precision loss and ensure accurate reward calculation even for small time differences or large total_supply values.

Validation steps

The multiplication factor DXLYN_DECIMAL used when computing reward_increment is too small and should be replaced with 2^64.

          let reward_increment =
                ((time_diff as u256) * (gauge.reward_rate) * (DXLYN_DECIMAL as u256))
                    / ((gauge.total_supply as u256) * PRECISION);


Otherwise, a malicious actor can repeatedly call the update (e.g. every few seconds or every block) with a tiny deposited balance so that reward_increment evaluates to zero due to precision loss. That leaves reward_per_token_stored unchanged while last_update_time keeps advancing, which eventually causes all users to lose accrued rewards. Replacing the small decimal factor with a much larger scale (e.g. 2^64) prevents this precision underflow and preserves correct reward accumulation even for very short update intervals.

Attachments

hidden
CommentsReport History
Comments on this report are hidden
Details
Statedisclosed
Severity
Medium
Bounty$100
Visibilitypartially
VulnerabilityBlockchain
Participants (4)
company admin
author
company admin