Last Man Standing

First Flight #45
Beginner FriendlyFoundrySolidity
100 EXP
View results
Submission Details
Severity: low
Valid

Use of block.timestamp Enables Game Result Manipulation by Miners

Use of block.timestamp Enables Game Result Manipulation by Miners

Description

  • The contract use block.timestamp to calculate lastClainTime + gracePeriod, and check if can call the declareWinner().

  • Miners can control the block.timestamp in some range(±15 seconds).That might influence the condition of calling declareWinner().In some extreme condition , the attacker would win the game unfairly by manipluate the block.timestamp.

// Root cause in the codebase with @> marks to highlight the relevant section
function declareWinner() external gameNotEnded {
require(
currentKing != address(0),
"Game: No one has claimed the throne yet."
);
require(
@> block.timestamp > lastClaimTime + gracePeriod,
"Game: Grace period has not expired yet."
);
gameEnded = true;
pendingWinnings[currentKing] = pendingWinnings[currentKing] + pot;
pot = 0; // Reset pot after assigning to winner's pending winnings
emit GameEnded(currentKing, pot, block.timestamp, gameRound);
}

Risk

Likelihood:

  • When the gracePeriod is near the miners control time window, malicious miner who is also a player can influence the result by modifing the block.timestamp.

Impact:

  • A malicious miner-player can unfairly claim the prize, depriving other participants.

Proof of Concept

  • A miner who is also a player (e.g. Alice) notices that she is the currentKing and that the condition block.timestamp > lastClaimTime + gracePeriod will soon be met within a few seconds.

  • By adjusting the block’s timestamp to T + gracePeriod + δ (where δ is within the miner’s ±15-second manipulation range), she forces the condition to succeed.

  • In that same block, she calls declareWinner(), prematurely ending the game and securing the victory.

  • Result: Alice unfairly ends the game early and claims the prize, denying other players a fair chance.

  • Impact example:https://www.cyfrin.io/glossary/block-timestamp-manipulation-hack-solidity-code-example

Recommended Mitigation

  • Use the block.number instead of block.timestamp for comparing.

// Game Core State
- uint256 public gracePeriod; // Time in seconds after which a winner can be declared (e.g., 24 hours)
+ uint256 public gracePeriod; // Block increase number atfer which a winner can be declared(e.g., 100 blocks)
- uint256 public lastClaimTime; // Timestamp when the throne was last claimed
+ uint256 public lastClaimBlockNumber; // block number which the throne was last claimed
constructor(
uint256 _initialClaimFee,
uint256 _gracePeriod,
uint256 _feeIncreasePercentage,
uint256 _platformFeePercentage
) Ownable(msg.sender) {
// Set deployer as owner
require(
_initialClaimFee > 0,
"Game: Initial claim fee must be greater than zero."
);
require(
_gracePeriod > 0,
"Game: Grace period must be greater than zero."
);
require(
_feeIncreasePercentage <= 100,
"Game: Fee increase percentage must be 0-100."
);
require(
_platformFeePercentage <= 100,
"Game: Platform fee percentage must be 0-100."
);
initialClaimFee = _initialClaimFee;
initialGracePeriod = _gracePeriod;
feeIncreasePercentage = _feeIncreasePercentage;
platformFeePercentage = _platformFeePercentage;
// Initialize game state for the first round
claimFee = initialClaimFee;
gracePeriod = initialGracePeriod;
- lastClaimTime = block.timestamp; // Game starts immediately upon deployment
+ lastClaimBlockNumber = block.number;//Game starts immediately upon deployment
gameRound = 1;
gameEnded = false;
// currentKing starts as address(0) until first claim
}
function declareWinner() external gameNotEnded {
require(
currentKing != address(0),
"Game: No one has claimed the throne yet."
);
require(
- block.timestamp > lastClaimTime + gracePeriod,
+ block.number > lastClaimBlockNumer + gracePeriod,
"Game: Grace period has not expired yet."
);
gameEnded = true;
pendingWinnings[currentKing] = pendingWinnings[currentKing] + pot;
pot = 0; // Reset pot after assigning to winner's pending winnings
emit GameEnded(currentKing, pot, block.timestamp, gameRound);
}
function resetGame() external onlyOwner gameEndedOnly {
currentKing = address(0);
- lastClaimTime = block.timestamp;
+ lastClaimBlokerNumber = block.number;
pot = 0;
claimFee = initialClaimFee;
gracePeriod = initialGracePeriod;
gameEnded = false;
gameRound = gameRound + 1;
// totalClaims is cumulative across rounds, not reset here, but could be if desired.
emit GameReset(gameRound, block.timestamp);
}
-function getRemainingTime() public view returns (uint256) {
- if (gameEnded) {
- return 0; // Game has ended, no remaining time
- }
- uint256 endTime = lastClaimTime + gracePeriod;
- if (block.timestamp >= endTime) {
- return 0; // Grace period has expired
- }
- return endTime - block.timestamp;
- }
+function getRemainingBlockNumber() public view returns (uint256) {
+ if (gameEnded) {
+ return 0; // Game has ended, no remaining time
+ }
+ uint256 endBlockNumber = lastClaimBlockNumber + gracePeriod;
+ if (block.number >= endBlockNumber) {
+ return 0; // Grace period has expired
+ }
+ return endBlockNumber - block.number;
+ }
Updates

Appeal created

inallhonesty Lead Judge about 2 months ago
Submission Judgement Published
Validated
Assigned finding tags:

block.timestamp in L2's

Support

FAQs

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