Beatland Festival

First Flight #44
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Impact: medium
Likelihood: low
Invalid

Timestamp Manipulation Vulnerability in Performance Cooldown System ( to get reward more than once)

Root + Impact

Description: The FestivalPass contract uses `block.timestamp` for cooldown checks between performance attendances. This implementation is vulnerable to timestamp manipulation by miners who can set block timestamps up to 15 minutes into the future.

  • Describe the normal behavior in one or more sentences: The normal behavior is an attendee to be able to call attendPerformance and get beatToken as reward based on a cooldown strategy.

  • Explain the specific issue or problem in one or more sentences: Malicious miners can help users bypass the cooldown period between performances by manipulating block timestamps, allowing them to earn BEAT tokens faster than intended.

// Root cause in the codebase with @> marks to highlight the relevant section
// Attend a performance to earn BEAT
function attendPerformance(uint256 performanceId) external {
require(isPerformanceActive(performanceId), "Performance is not active");
require(hasPass(msg.sender), "Must own a pass");
require(!hasAttended[performanceId][msg.sender], "Already attended this performance");
@> require(block.timestamp >= lastCheckIn[msg.sender] + COOLDOWN, "Cooldown period not met");
hasAttended[performanceId][msg.sender] = true;
lastCheckIn[msg.sender] = block.timestamp;
uint256 multiplier = getMultiplier(msg.sender);
BeatToken(beatToken).mint(msg.sender, performances[performanceId].baseReward * multiplier);
emit Attended(msg.sender, performanceId, performances[performanceId].baseReward * multiplier);
}

Risk

Likelihood:

  • Requires coordination between multiple miners

  • But is technically feasible

  • Has financial incentive (earning BEAT tokens faster)

  • MIners can do this ( but in that case all attendeeds can exploit)

Impact:

  • Can bypass the 1-hour cooldown much faster than intended

  • Each miner in sequence can push time forward by 15 minutes

  • Could compress 1 hour into much less real time

  • Allows earning BEAT tokens faster than protocol design

Proof of Concept

lBock 1: timestamp = 12:00:00
Block 2: timestamp = 12:15:00 (+15 min max allowed)
Block 3: timestamp = 12:30:00 (+15 min again)
Block 4: timestamp = 12:45:00 (+15 min again)
Block 5: timestamp = 13:00:00 (+15 min again)
By chaining multiple blocks with maximum forward timestamp manipulation,
miners could actually make it appear that 1 hour has passed in much less real time! This means:
function test_TimestampManipulationVulnerability() public {
// Setup simple EOA attacker
address attacker = makeAddr("attacker");
vm.deal(attacker, 1 ether);
// Create two performances
vm.startPrank(organizer);
uint256 perfId1 = festivalPass.createPerformance(
block.timestamp + 1 hours,
2 hours,
100 ether // 100 BEAT tokens
);
uint256 perfId2 = festivalPass.createPerformance(
block.timestamp + 1 hours,
2 hours,
100 ether
);
vm.stopPrank();
// Warp to performance time
vm.warp(block.timestamp + 90 minutes);
// First attendance
vm.startPrank(attacker);
festivalPass.buyPass{value: 0.05 ether}(1);
festivalPass.attendPerformance(perfId1);
uint256 initialBalance = beatToken.balanceOf(attacker);
// Try immediate reattendance of second performance - should fail
vm.expectRevert("Cooldown period not met");
festivalPass.attendPerformance(perfId2);
// Simulate miner timestamp manipulation (15 min forward each block)
vm.warp(block.timestamp + 15 minutes); // Block N+1
vm.expectRevert("Cooldown period not met");
festivalPass.attendPerformance(perfId2);
vm.warp(block.timestamp + 15 minutes); // Block N+2
vm.expectRevert("Cooldown period not met");
festivalPass.attendPerformance(perfId2);
vm.warp(block.timestamp + 15 minutes); // Block N+3
vm.expectRevert("Cooldown period not met");
festivalPass.attendPerformance(perfId2);
vm.warp(block.timestamp + 15 minutes); // Block N+4
festivalPass.attendPerformance(perfId2);
vm.stopPrank();
// Check attacker got rewards twice
uint256 finalBalance = beatToken.balanceOf(attacker);
assertEq(
finalBalance,
initialBalance + 100 ether,
"Got second reward through timestamp manipulation"
);
}

Recommended Mitigation

+ mapping(address => uint256) public lastCheckInBlock;
+ uint256 constant BLOCK_COOLDOWN = 300; // approx 1 hour
function attendPerformance(uint256 performanceId) external {
require(isPerformanceActive(performanceId), "Performance is not active");
require(hasPass(msg.sender), "Must own a pass");
require(!hasAttended[performanceId][msg.sender], "Already attended this performance");
- require(block.timestamp >= lastCheckIn[msg.sender] + COOLDOWN, "Cooldown period not met");
+ require(block.number >= lastCheckInBlock[msg.sender] + BLOCK_COOLDOWN, "Cooldown period not met");
hasAttended[performanceId][msg.sender] = true;
- lastCheckIn[msg.sender] = block.timestamp;
+ lastCheckInBlock[msg.sender] = block.number;
uint256 multiplier = getMultiplier(msg.sender);
BeatToken(beatToken).mint(msg.sender, performances[performanceId].baseReward * multiplier);
emit Attended(msg.sender, performanceId, performances[performanceId].baseReward * multiplier);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.