49939 sc high initial timestamp mismatch might lead to users being able to spin twice in the same day
Submitted on Jul 20th 2025 at 16:45:07 UTC by @a16 for Attackathon | Plume Network
Report ID: #49939
Report Type: Smart Contract
Report severity: High
Target: https://github.com/immunefi-team/attackathon-plume-network/blob/main/plume/src/spin/Spin.sol
Impacts:
Contract fails to deliver promised returns, but doesn't lose value
Description
Brief / Intro
Non-whitelisted users should only be able to "spin" once per day, which is significant because winning odds vary by day of the week. Due to a potential mismatch between the initial timestamp used to calculate the day in the canSpin() modifier and the getCurrentWeek() function (and other reward/jackpot computations), users could potentially circumvent the once-per-day restriction.
Vulnerability Details
Two different parts of the code determine the day differently:
The restriction that prevents users from spinning more than once a day relies on
DateTime.solviadateTime.getDay(), used by thecanSpin()modifier.getDay()is based on the integer division of the current timestamp by DAY_IN_SECONDS (86400).Rewards and jackpot day calculations are based on
campaignStartDate. For example,jackpotThresholduses:
uint256 daysSinceStart = (block.timestamp - campaignStartDate) / 1 days;
uint8 dayOfWeek = uint8(daysSinceStart % 7);The setter for campaignStartDate:
function setCampaignStartDate(
uint256 start
) external onlyRole(ADMIN_ROLE) {
campaignStartDate = start == 0 ? block.timestamp : start;
}Because campaignStartDate does not have to be divisible by 86400 (it may be set to an arbitrary timestamp like block.timestamp when the admin calls setCampaignStartDate()), it is possible that the two day calculations disagree. Concretely, canSpin() might consider two timestamps to be different calendar days (using timestamp // 86400), while (block.timestamp - campaignStartDate) / 1 days yields the same day index for rewards/jackpot. This discrepancy can let a user effectively "spin twice" for the same reward-day bucket while passing the canSpin() check.
Impact Details
Savvy users could exploit this mismatch to increase their effective chances of winning or obtaining better rewards. For example, the jackpot probabilities per day could be:
jackpotProbabilities = [1, 2, 3, 5, 7, 10, 20];
If one day is much more favorable (e.g., 20x chance on a particular day), a user could make two spins that the modifier allows as two different calendar days, but the reward logic treats them as the same day relative to campaignStartDate, thereby unfairly amplifying their odds.
Suggestion
Proof of Concept
Was this helpful?