Beatland Festival

AI First Flight #4
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Impact: low
Likelihood: high
Invalid

[M-1] `FestivalPass::attendPerformance` relies on `block.timestamp` which can be manipulated by miners

[M-1] `FestivalPass::attendPerformance` relies on `block.timestamp` which can be manipulated by miners

Description

  • The `FestivalPass::attendPerformance` function uses a 1-hour cooldown to prevent users from attending performances.

  • However, this function relies on the `lastCheckIn` mapping, which maps a user to a `uint256` timestamp (`block.timestamp`) that can be manipulated by malicious miners.

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:

  • When a malicious miner manipulates `block.timestamp`

Impact:

  • User can't attend a specific performance due to function requirement

  • user can't win BEAT tokens

Proof of Concept

1. Organizer creates performances

1. Performance 1 is 1 hour long and starts in an hour

2. Performance 2 is 1 hour long and starts 30 min after the first

2. User buys a pass

3. User attends performance 1

4. Malicious miner goes back 30 minutes

5. User wants to attend performance 2 but it is not active, so it reverts

function testBlockTimestampManipulationPreventsAttendingPerformance() public {
// Organizer creates performances
vm.startPrank(organizer);
uint256 startTime1 = block.timestamp + 1 hours;
uint256 startTime2 = startTime1 + 30 minutes;
uint256 perfId1 = festivalPass.createPerformance(startTime1, 1 hours, 1000e18);
uint256 perfId2 = festivalPass.createPerformance(startTime2, 1 hours, 1000e18);
vm.stopPrank();
// User buys a pass
vm.prank(user1);
festivalPass.buyPass{value: GENERAL_PRICE}(1);
// It's time for performance 1
vm.warp(startTime1 + 30 minutes);
// User attends performance 1
vm.prank(user1);
festivalPass.attendPerformance(perfId1);
// Malicious miner goes back 30 minutes
vm.warp(startTime1 - 30 minutes);
// User wants to attend performance 2
vm.prank(user1);
vm.expectRevert("Performance is not active");
festivalPass.attendPerformance(perfId2);
}

Recommended Mitigation

Since `block.timestamp` is manipulatable, consider using `block.number` instead.

- uint256 constant COOLDOWN = 1 hours;
+ uint256 constant COOLDOWN = 300; // number of blocks in an hour (1 block every ~12s)
// ...
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 >= lastCheckIn[msg.sender] + COOLDOWN, "Cooldown period not met");
hasAttended[performanceId][msg.sender] = true;
- lastCheckIn[msg.sender] = block.timestamp;
+ lastCheckIn[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

ai-first-flight-judge Lead Judge about 22 hours ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!