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);
}
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 {
address attacker = makeAddr("attacker");
vm.deal(attacker, 1 ether);
vm.startPrank(organizer);
uint256 perfId1 = festivalPass.createPerformance(
block.timestamp + 1 hours,
2 hours,
100 ether
);
uint256 perfId2 = festivalPass.createPerformance(
block.timestamp + 1 hours,
2 hours,
100 ether
);
vm.stopPrank();
vm.warp(block.timestamp + 90 minutes);
vm.startPrank(attacker);
festivalPass.buyPass{value: 0.05 ether}(1);
festivalPass.attendPerformance(perfId1);
uint256 initialBalance = beatToken.balanceOf(attacker);
vm.expectRevert("Cooldown period not met");
festivalPass.attendPerformance(perfId2);
vm.warp(block.timestamp + 15 minutes);
vm.expectRevert("Cooldown period not met");
festivalPass.attendPerformance(perfId2);
vm.warp(block.timestamp + 15 minutes);
vm.expectRevert("Cooldown period not met");
festivalPass.attendPerformance(perfId2);
vm.warp(block.timestamp + 15 minutes);
vm.expectRevert("Cooldown period not met");
festivalPass.attendPerformance(perfId2);
vm.warp(block.timestamp + 15 minutes);
festivalPass.attendPerformance(perfId2);
vm.stopPrank();
uint256 finalBalance = beatToken.balanceOf(attacker);
assertEq(
finalBalance,
initialBalance + 100 ether,
"Got second reward through timestamp manipulation"
);
}
+ 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);
}