Immunefi Audit Competitions
Plume | Attackathon

50796 sc high jackpot eligibility uses stale streak

Submitted on Jul 28th 2025 at 16:24:35 UTC by @BeastBoy for Attackathon | Plume Network

Description

In handleRandomness the code begins by computing:

currentSpinStreak = _computeStreak(user, block.timestamp, true)

but then, inside the “Jackpot” branch, evaluates:

else if (userDataStorage.streakCount < (currentWeek + 2)) {
    userDataStorage.nothingCounts += 1;
    rewardCategory = "Nothing";
}

Here userDataStorage.streakCount still holds yesterday’s value, so even if currentSpinStreak meets the required threshold (currentWeek + 2), the check fails. Only after this logic does the code execute:

userDataStorage.streakCount = currentSpinStreak;

meaning the first day a user actually reaches the needed streak they are incorrectly treated as ineligible.

Impact

Users must spin an extra day before becoming eligible for the jackpot, contrary to intended behavior and degrading user experience.

Recommendation

Assign userDataStorage.streakCount = currentSpinStreak before the jackpot check or change the condition to compare currentSpinStreak directly against the required threshold so that today’s spin counts immediately.

Proof of Concept

PoC: testStreakJackpotBug.sol

function testStreakJackpotBug() public {
    // Set up Week 0 (requires 2-day streak)
    vm.warp(spin.getCampaignStartDate());
    
    // Day 1: User spins for first time
    vm.deal(USER, INITIAL_SPIN_PRICE);
    uint256 nonce1 = performPaidSpin(USER);
    
    // Mock jackpot-winning randomness but expect denial due to insufficient old streak
    uint256[] memory rng1 = new uint256[](1);
    rng1[0] = 0; // Force jackpot
    
    vm.prank(SUPRA_ORACLE);
    vm.recordLogs();
    spin.handleRandomness(nonce1, rng1);
    
    Vm.Log[] memory logs1 = vm.getRecordedLogs();
    (string memory result1,) = abi.decode(logs1[logs1.length - 1].data, (string, uint256));
    assertEq(result1, "Nothing", "Day 1: Should be denied (old streak = 0)");
    
    // Verify current streak is now 1
    assertEq(spin.currentStreak(USER), 1, "Current streak should be 1 after day 1");
    
    // Day 2: User spins again (should have 2-day streak but gets denied)
    vm.warp(block.timestamp + 1 days);
    vm.deal(USER, INITIAL_SPIN_PRICE);
    uint256 nonce2 = performPaidSpin(USER);
    
    // Before the spin, verify the user SHOULD be eligible
    uint256 wouldBeStreak = spin.currentStreak(USER); // This will be 1 (old)
    // But _computeStreak(USER, block.timestamp, true) would return 2
    
    uint256[] memory rng2 = new uint256[](1);
    rng2[0] = 0; // Force jackpot
    
    vm.prank(SUPRA_ORACLE);
    vm.recordLogs();
    spin.handleRandomness(nonce2, rng2);
    
    Vm.Log[] memory logs2 = vm.getRecordedLogs();
    (string memory result2,) = abi.decode(logs2[logs2.length - 1].data, (string, uint256));
    
    // THE BUG: User should get jackpot (actual streak = 2) but gets denied (old streak = 1)
    assertEq(result2, "Nothing", "Day 2: BUG - User denied despite having 2-day streak");
    
    // Day 3: Now user finally gets jackpot
    vm.warp(block.timestamp + 1 days);
    vm.deal(USER, INITIAL_SPIN_PRICE);
    uint256 nonce3 = performPaidSpin(USER);
    
    uint256[] memory rng3 = new uint256[](1);
    rng3[0] = 0; // Force jackpot
    
    vm.prank(SUPRA_ORACLE);
    vm.recordLogs();
    spin.handleRandomness(nonce3, rng3);
    
    Vm.Log[] memory logs3 = vm.getRecordedLogs();
    (string memory result3,) = abi.decode(logs3[logs3.length - 1].data, (string, uint256));
    
    assertEq(result3, "Jackpot", "Day 3: Finally eligible (one day late)");
    
    console2.log("BUG CONFIRMED: User eligible on day 2 but denied until day 3");
}