Root + Impact
Description
-
Normal Behavior: When a player claims the throne, a small portion of the claimFee
should be paid to the previous king as a reward. This is stated in both code comments and the project description.
-
Issue: The variable previousKingPayout
is declared but never assigned any non-zero value. It remains 0
in all cases, and no actual transfer to the previous king is made, breaking the intended reward mechanism.
function claimThrone() external payable gameNotEnded nonReentrant {
...
uint256 previousKingPayout = 0;
...
if (currentPlatformFee > (sentAmount - previousKingPayout)) {
currentPlatformFee = sentAmount - previousKingPayout;
}
...
}
Risk
Likelihood:
-
This issue occurs on every claimThrone()
call after the first claim.
-
Once there’s a king, the payout logic should activate — but it silently fails.
Impact:
Proof of Concept
pragma solidity ^0.8.20;
import {Test, console2} from "forge-std/Test.sol";
import {Game} from "../src/Game.sol";
contract GameTest is Test {
Game public game;
address public deployer;
address public player1;
address public player2;
address public player3;
address public maliciousActor;
uint256 public constant INITIAL_CLAIM_FEE = 0.1 ether;
uint256 public constant GRACE_PERIOD = 1 days;
uint256 public constant FEE_INCREASE_PERCENTAGE = 10;
uint256 public constant PLATFORM_FEE_PERCENTAGE = 5;
function setUp() public {
deployer = makeAddr("deployer");
player1 = makeAddr("player1");
player2 = makeAddr("player2");
player3 = makeAddr("player3");
maliciousActor = makeAddr("maliciousActor");
vm.deal(deployer, 10 ether);
vm.deal(player1, 10 ether);
vm.deal(player2, 10 ether);
vm.deal(player3, 10 ether);
vm.deal(maliciousActor, 10 ether);
vm.startPrank(deployer);
game = new Game(
INITIAL_CLAIM_FEE,
GRACE_PERIOD,
FEE_INCREASE_PERCENTAGE,
PLATFORM_FEE_PERCENTAGE
);
vm.stopPrank();
}
function test_PreviousKinkDoesNotReceivePayout() public {
vm.deal(player1, 1 ether);
vm.startPrank(player1);
game.claimThrone{value: 1 ether}();
vm.stopPrank();
console2.log(
"Player1 balance after claiming throne: ",
player1.balance
);
vm.deal(player2, 2 ether);
vm.startPrank(player2);
game.claimThrone{value: 2 ether}();
vm.stopPrank();
console2.log(
"Player1 balance after player2 claims throne: ",
player1.balance
);
assertEq(
player1.balance,
0,
"Player1 should have received payout but didn't"
);
}
}
Recommended Mitigation
...
// Game Core State
...
+ uint256 previousKingRewardPercentage = 10; // For calculate previuosKinh payout
...
function claimThrone() external payable gameNotEnded nonReentrant {
...
uint256 sentAmount = msg.value;
- uint256 previousKingPayout = 0;
- uint256 currentPlatformFee = 0;
- uint256 amountToPot = 0;
+ // Payout to the previous king
+ uint256 previousKingPayout = (sentAmount * previousKingRewardPercentage) / 100;
+
+ if (currentKing != address(0) && previousKingPayout > 0) {
+ (bool sent, ) = currentKing.call{value: previousKingPayout}("");
+ require(sent, "Failed to send payout to previous king");
+ }
+
+ // Calculate platform fee and amount added to the pot
+ uint256 remaining = sentAmount - previousKingPayout;
+ uint256 currentPlatformFee = (remaining * platformFeePercentage) / 100;
+
+ // Safety check: fee should not exceed the remaining amount
+ if (currentPlatformFee > remaining) {
+ currentPlatformFee = remaining;
+ }
+ platformFeesBalance += currentPlatformFee;
+
+ uint256 amountToPot = remaining - currentPlatformFee;
+ pot += amountToPot;
- // Calculate platform fee
- currentPlatformFee = (sentAmount * platformFeePercentage) / 100;
-
- // Defensive check to ensure platformFee doesn't exceed available amount after previousKingPayout
- if (currentPlatformFee > (sentAmount - previousKingPayout)) {
- currentPlatformFee = sentAmount - previousKingPayout;
- }
- platformFeesBalance = platformFeesBalance + currentPlatformFee;
-
- // Remaining amount goes to the pot
- amountToPot = sentAmount - currentPlatformFee;
- pot = pot + amountToPot;
// Update game state
currentKing = msg.sender;
...