50487 sc low cross campaign jackpot denial due to state pollution
Submitted on Jul 25th 2025 at 10:19:28 UTC by @IronsideSec for Attackathon | Plume Network
Report ID: #50487
Report Type: Smart Contract
Report severity: Low
Target: https://github.com/immunefi-team/attackathon-plume-network/blob/main/plume/src/spin/Spin.sol
Impacts: Theft of unclaimed yield
Description
Brief / Intro
The Spin.sol contract suffers from a state pollution vulnerability where lastJackpotClaimWeek is not reset when a new campaign starts via setCampaignStartDate(). This can cause legitimate users in subsequent campaigns to be denied their jackpot prizes if they win a jackpot in a week number that was already used by a previous campaign's jackpot winner. Losses can be up to 100,000 PLUME tokens per affected user (final week 12 jackpot).
See the Recommended Fix below for an easy mitigation.
Vulnerability Details
The proof-of-concept test demonstrates this scenario:
Campaign 1 jackpot in week 5: ✅ 20,000 PLUME awarded
Campaign 2 jackpot in week 5: ❌ 0 PLUME awarded (denied)
Root Cause - Incomplete State Reset
The admin function that starts a new campaign updates campaignStartDate but does not reset lastJackpotClaimWeek, leaving stale state that is used by the jackpot validation logic.
Vulnerable setter:
// attackathon-plume-network/plume/src/spin/Spin.sol:479-483
function setCampaignStartDate(
uint256 start
) external onlyRole(ADMIN_ROLE) {
campaignStartDate = start == 0 ? block.timestamp : start;
// ❌ BUG: lastJackpotClaimWeek is NOT reset here
}Vulnerable jackpot validation:
// attackathon-plume-network/plume/src/spin/Spin.sol:246-249
if (keccak256(bytes(rewardCategory)) == keccak256("Jackpot")) {
uint256 currentWeek = getCurrentWeek();
if (currentWeek == lastJackpotClaimWeek) { // ❌ Uses stale data from previous campaign
userDataStorage.nothingCounts += 1;
rewardCategory = "Nothing";
rewardAmount = 0;
emit JackpotAlreadyClaimed("Jackpot already claimed this week");Week calculation:
// attackathon-plume-network/plume/src/spin/Spin.sol:207-209
function getCurrentWeek() public view returns (uint256) {
return (block.timestamp - campaignStartDate) / 7 days;
}Impact Details
Direct per-user loss: up to 100,000 PLUME tokens (week 11/12 jackpot).
Affected users: users with long streaks (13+ day streaks) who legitimately earn jackpots.
Spin fee loss: 2 PLUME per denied spin (no refund mechanism).
Systemic: weeks where jackpots were previously claimed become blocked across campaigns if the same week number occurs again; this effectively lets earlier campaigns "reserve" jackpot weeks for future campaigns, reducing availability and harming high-value users disproportionately.
Severity multipliers:
Users cannot control or predict this.
Affects legitimately earned jackpots.
No recovery mechanism for denied jackpots.
Cumulative across campaigns.
Recommended Fix
Reset lastJackpotClaimWeek when starting a new campaign so prior campaign data cannot block jackpot claims in the new campaign.
Suggested change:
function setCampaignStartDate(uint256 start) external onlyRole(ADMIN_ROLE) {
campaignStartDate = start == 0 ? block.timestamp : start;
lastJackpotClaimWeek = 999; // Reset to initial state for new campaign
}(Use an appropriate sentinel value consistent with the contract's initialization behavior — e.g., a very large number or the original initial value used in contract deployment.)
Proof of Concept
Gist with PoC and test steps: https://gist.github.com/IronsideSec/107fd6c7c13961071c2a7bf52de339d2
Steps (reproduced from the Gist) shown as a stepper:
User1 spins daily for 7 days in Campaign 1 to build streak.
User1 hits jackpot in week 5 of Campaign 1 → lastJackpotClaimWeek = 5. Campaign 1 ends.
Admin starts Campaign 2 with setCampaignStartDate() → BUG: lastJackpotClaimWeek still = 5.
User2 spins daily for 7 days in Campaign 2 to build streak.
User2 hits jackpot in week 5 of Campaign 2 (winning RNG).
Contract checks: currentWeek (5) == lastJackpotClaimWeek (5) → TRUE.
Contract emits JackpotAlreadyClaimed and converts reward to "Nothing".
User2 loses the 2 PLUME spin fee and gets 0 payout despite winning RNG.
Fix: Reset lastJackpotClaimWeek in setCampaignStartDate() function.
References
Vulnerable Function: setCampaignStartDate() in Spin.sol:479-483
Jackpot Validation: handleRandomness() in Spin.sol
Week Calculator: getCurrentWeek() in Spin.sol:207-209
Link to Proof of Concept
https://gist.github.com/IronsideSec/107fd6c7c13961071c2a7bf52de339d2
Was this helpful?