Massive Frost Yak
Medium
Integer division in time calculations will cause a precision loss for users as the contract will round down timestamps, potentially leading to shorter lock durations or miscalculated reward periods.
In checkIncreaseUnlockTime and notifyRewardAmounts, the calculation (block.timestamp / 1 weeks) * 1 weeks
performs integer division before multiplication, which causes precision loss.
- The contract needs to be deployed and initialized.
- Users need to interact with functions that rely on these time calculations, such as
checkIncreaseUnlockTime
ornotifyRewardAmounts
.
None
- A user calls a function that uses
checkIncreaseUnlockTime
, such asvote
orpoke
. - The function calculates the unlock time using
(block.timestamp / 1 weeks) * 1 weeks
. - Due to integer division, the calculated time is rounded down to the nearest week.
- This results in a shorter lock duration than intended.
Let's consider an example with a non-aligned duration:
Assume lockDuration
is 1,234,567 seconds (about 14 days, 6 hours, 56 minutes, and 7 seconds)
In this case, you can see that the unlock time has been rounded down to the nearest week boundary. The extra 6 hours, 56 minutes, and 7 seconds were truncated, ensuring the unlock happens at the start of the week.
Similarly for notifyRewardAmounts
:
- An operator calls
notifyRewardAmounts
to distribute rewards. - The function calculates the current period using
(block.timestamp / 1 weeks) * 1 weeks
. - Due to integer division, the calculated period is rounded down to the nearest week.
- This may result in incorrect period calculations and potentially affect reward distributions.
The same rounding down effect occurs in this function, potentially leading to misaligned reward periods.
Users may experience shorter lock durations than intended, potentially reducing their voting power or rewards. The protocol may also face issues with reward period calculations, leading to inconsistencies in reward distributions. As demonstrated in the example, users could lose up to 6 days, 23 hours, 59 minutes, and 59 seconds of lock time in the worst case. While the impact per transaction might seem small, it could accumulate over time and affect many users, especially those who frequently interact with the contract.
To Test the POC follow the below steps:
# To run the following POC paste the below code in BugTest.sol file
# Type the following command
forge test --mt test_PrecisionLoss -vvvv
Here is the POC demonstrating the precision loss using stateless fuzzing:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.10;
import {Test} from "forge-std/Test.sol";
contract BugTest is Test{
function setUp() public {}
function test_PrecisionLoss(uint256 time) public {
// Min: Block.timestamp is 1728727271 => IST 2024-10-12 03:31:00 PM
// Max: 100_000 days = 8640000000 seconds
uint BoundedTime = bound(time, 1728727271, 8640000000);
uint256 currentPeriod = (time / 1 weeks) * 1 weeks;
assertEq(currentPeriod, BoundedTime, "Precision Loss");
}
}
Here is the stack trace of the failed test:
Ran 1 test for test/BugTest.sol:BugTest
[FAIL. Reason: Precision Loss: 3424377600 != 3424681274; counterexample: calldata=0x8ef8fa75000000000000000000000000000000000000000000000007608b0c13426c7dde args=[136083875842229042654 [1.36e20]]] test_PrecisionLoss(uint256) (runs: 0, μ: 0, ~: 0)
Logs:
Bound result 3424681274
Traces:
[174] BugTest::setUp()
└─ ← [Return]
[7383] BugTest::test_PrecisionLoss(136083875842229042654 [1.36e20])
├─ [0] console::log("Bound result", 3424681274 [3.424e9]) [staticcall]
│ └─ ← [Stop]
├─ [0] VM::assertEq(3424377600 [3.424e9], 3424681274 [3.424e9], "Precision Loss") [staticcall]
│ └─ ← [Revert] Precision Loss: 3424377600 != 3424681274
└─ ← [Revert] Precision Loss: 3424377600 != 3424681274
Suite result: FAILED. 0 passed; 1 failed; 0 skipped;
To mitigate this issue, consider using a more precise calculation method. One approach is to store the start of the first week as a constant and calculate periods based on that. For example:
uint256 public constant WEEK_START = 1672531200; // Example: Jan 1, 2023 00:00:00 UTC
function calculateWeek(uint256 timestamp) public view returns (uint256) {
return (timestamp - WEEK_START) / 1 weeks;
}
function calculateWeekStart(uint256 timestamp) public view returns (uint256) {
return WEEK_START + (calculateWeek(timestamp) * 1 weeks);
}