50694 sc low spins occuring close to midnight lead to users streaks being unfairly broken due to vrf callback delay
Submitted on Jul 27th 2025 at 15:52:29 UTC by @heavyw8t for Attackathon | Plume Network
Report ID: #50694
Report Type: Smart Contract
Report severity: Low
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
The Spin contract contains a timing vulnerability where users can lose their daily streaks due to VRF callback delays. When a user initiates a spin near midnight, the VRF callback may execute after the day boundary, causing the streak calculation to incorrectly reset the user's streak to 1.
Vulnerability Details
The issue stems from the separation of streak validation and streak calculation across two different transactions with different timestamps. The callback function handleRandomness() — which is called after a delay by the VRF Oracle — actually calculates if the streak has been broken, using the timestamp when the callback occurred, not the timestamp of when the user called the spin() function. Since the delay between calling spin() and the callback occurring is not influenced in any way by the user, this can lead to the user losing their streak unfairly and therefore losing yield as they will now be ineligible for the jackpot and receive less raffle rewards.
In startSpin(), the canSpin modifier only validates that the user hasn't already spun today:
// plume/src/spin/Spin.sol:146-164
modifier canSpin() {
// Early return if the user is whitelisted
if (whitelists[msg.sender]) {
_;
return;
}
UserData storage userDataStorage = userData[msg.sender];
uint256 _lastSpinTimestamp = userDataStorage.lastSpinTimestamp;
// Retrieve last spin date components
(uint16 lastSpinYear, uint8 lastSpinMonth, uint8 lastSpinDay) = (
dateTime.getYear(_lastSpinTimestamp),
dateTime.getMonth(_lastSpinTimestamp),
dateTime.getDay(_lastSpinTimestamp)
);
// Retrieve current date components
(uint16 currentYear, uint8 currentMonth, uint8 currentDay) =
(dateTime.getYear(block.timestamp), dateTime.getMonth(block.timestamp), dateTime.getDay(block.timestamp));
// Ensure the user hasn't already spun today
if (isSameDay(lastSpinYear, lastSpinMonth, lastSpinDay, currentYear, currentMonth, currentDay)) {
revert AlreadySpunToday();
}
_;
}However, in handleRandomness(), the actual streak calculation uses a different timestamp:
The _computeStreak() function determines streak continuation using simple day arithmetic:
Impact Details
Streak Loss: Users lose accumulated daily streaks despite properly initiating spins on consecutive days.
Jackpot Eligibility: Users may become ineligible for jackpot claims due to artificially broken streaks.
Fewer Raffle rewards: Users will receive less raffle rewards as their multiplier is now smaller.
Proof of Concept
Result: User loses 5-day streak despite valid consecutive daily participation.
References
Was this helpful?