https://github.com/hackenproof-public/tokenomics_contract
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.
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.