F-2024-0001·precision-loss

Precision loss in _cycleEnd calculation

Acknowledgedvaultyieldbtc
TL;DR

previewSyncRewards uses integer division then multiplication on the same variable to compute _cycleEnd, losing precision and producing reward cycles that drift from the expected REWARDS_CYCLE_LENGTH interval.

Severity
MEDIUM
Impact
MEDIUM
Likelihood
MEDIUM
Method
MManual review
CAT.
Complexity
LOW
Exploitability
LOW
02Section · Description

Description

In the previewSyncRewards() function, there is a potential precision loss when calculating the value of _cycleEnd. The calculation uses division and multiplication operations with integers, which can result in a loss of precision due to rounding.

solidity
function test_PrecisionLossIn_cycleEnd(uint256 _amount, uint256 _timePassed) public {
vm.assume(_amount > 0 && _amount <= 1000 ether);
vm.assume(_timePassed > 1 days && _timePassed <= 7 days);
uint40 cycleEndBefore = stakedEbtc.__rewardsCycleData().cycleEnd;
uint40 lastSyncBefore = stakedEbtc.__rewardsCycleData().lastSync;
uint216 rewardCycleAmountBefore = stakedEbtc.__rewardsCycleData().rewardCycleAmount;
vm.prank(bob);
uint256 shares = stakedEbtc.deposit(_amount, bob);
uint256 cycleLength = stakedEbtc.REWARDS_CYCLE_LENGTH();
uint256 timeBefore = block.timestamp + _timePassed;
// Warp to a time near the end of the cycle
uint256 timeNearCycleEnd = timeBefore + cycleLength - (cycleLength / 40);
vm.warp(timeNearCycleEnd);
uint256 _timestamp = block.timestamp;
uint40 cycleEndUnoptimized = (((_timestamp + cycleLength) / cycleLength) * cycleLength).safeCastTo40();
uint40 cycleEndOptimized = (((_timestamp + cycleLength))).safeCastTo40();
if (cycleEndOptimized - _timestamp < cycleLength / 40) {
cycleEndOptimized += cycleLength.safeCastTo40();
}
if (cycleEndUnoptimized - _timestamp < cycleLength / 40) {
cycleEndUnoptimized += cycleLength.safeCastTo40();
}
stakedEbtc.syncRewardsAndDistribution();
assertEq(cycleEndInContract, cycleEndOptimized, "Cycle end should match the calculated value");
assertEq(cycleEndUnoptimized, cycleEndOptimized, "Precision loss");
}

Console output (extract):

code
[FAIL: Cycle end should match the calculated value: 1209600 != 1410866;
=== Cycle End Comparison ===
Contract Cycle End: 1209600
Test Optimized Cycle End: 1410866
Test Unoptimized Cycle End: 1209600
cycleEndUnoptimized != cycleEndOptimized
=========================
=== Precision Analysis ===
Difference between Optimized and Unoptimized: 201266
=========================
03Section · Impact

Impact

The precision loss in the calculation of _cycleEnd could result in reward cycles that do not perfectly align with the expected time intervals. This leads to small discrepancies in the distribution of rewards over time, as the amount of rewards will be smaller than it is meant to be.

04Section · Recommendation

Recommendation

Simplify the formula for calculating _cycleEnd by removing the redundant operation of multiplying and dividing by the same variable REWARDS_CYCLE_LENGTH.

The optimized formula would be:

solidity
uint40 _cycleEnd = (_timestamp + REWARDS_CYCLE_LENGTH).safeCastTo40();
F-2024-0001

oog
zealynx

Smart Contract Security Digest

Monthly exploit breakdowns, audit checklists, and DeFi security research — straight to your inbox

© 2026 Zealynx