Status DataClose notification

DexLyn Disclosed Report

Audit Report

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

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

Attachments are hidden

This information is not publicly available.

Comments are hidden

This information is not publicly available.