Summary
The contract lacks an initial pot value in its constructor, meaning the first participant must fund the game's pot entirely through their own claim. After deducting the platform fee, this initial contribution becomes the rollover amount. If no subsequent participants join and game grace period has passed, the first claimer, despite being the winner, receives less than their original claim amount. This disincentivizes users from initiating the game, leading to low participation and making the game less appealing at start.
Description
The constructor does not initialize the game with a starting pot value for the first round. As a result, the first player to call the claimThrone
function must fully fund the pot from their own claim amount. After the platform fee is deducted, the resulting pot is smaller than the amount sent by the first claimer. If no additional players join and the grace period ends, the first claimer though being declared the winner, receives a payout that is less than their original claim. This creates a disincentive for users to be the first mover, leading to hesitation or reluctance to initiate the game.
Consequently, players may wait for others to start the game, potentially resulting in zero participation unless the owner or deployer initiates the first claim.
...
<@@> uint256 public pot;
...
constructor(
uint256 _initialClaimFee,
uint256 _gracePeriod,
uint256 _feeIncreasePercentage,
uint256 _platformFeePercentage
) Ownable(msg.sender) {
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;
<@@>
claimFee = initialClaimFee;
gracePeriod = initialGracePeriod;
lastClaimTime = block.timestamp;
gameRound = 1;
gameEnded = false;
}
Risk
Likelihood:
Impact:
Proof of Concept
In test/Game.t.sol
, add the following test:
function test_audit_disadvantageAsFirstClaimer() public {
uint256 player1Balance_before = player1.balance;
console2.log("player1Balance_before: ", player1Balance_before);
vm.prank(player1);
game.claimThrone{value: INITIAL_CLAIM_FEE}();
vm.warp(game.lastClaimTime() + GRACE_PERIOD + 1 minutes);
game.declareWinner();
vm.prank(player1);
game.withdrawWinnings();
uint256 player1Balance_after = player1.balance;
console2.log("player1Balance_after: ", player1Balance_after);
assert(player1Balance_after < player1Balance_before);
}
In terminal, run forge test --match-test test_audit_disadvantageAsFirstClaimer -vvv
will generate the following results:
$ forge test --match-test test_audit_disadvantageAsFirstClaimer -vvv
...
Ran 1 test for test/Game.t.sol:GameTest
[PASS] test_audit_disadvantageAsFirstClaimer() (gas: 204031)
Logs:
player1Balance_before: 10000000000000000000
player1Balance_after: 9995000000000000000
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 728.38µs (88.33µs CPU time)
The test showed that the player 1 being the first claimer and sole winner eventually got back amount that is less than what he had initially.
Recommended Mitigation
To add an initial pot value during the initialization of the game states for the first round at contructor and include initial pot value in resetGame
function. Also, add a new function to allow the update of the initial pot value after deployment.
...
// Game Parameters (Configurable by Owner)
uint256 public initialClaimFee; // The starting fee for a new game round
uint256 public feeIncreasePercentage; // Percentage by which the claimFee increases after each successful claim (e.g., 10 for 10%)
uint256 public platformFeePercentage; // Percentage of the claimFee that goes to the contract owner (deployer)
uint256 public initialGracePeriod; // The grace period set at the start of a new game round
+ uint256 public initialPotValue;
...
constructor(
uint256 _initialClaimFee,
uint256 _gracePeriod,
+ uint256 _initialPotValue,
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(_initialPotValue > 0, "Game: Initial pot value 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;
+ initialPotValue = _initialPotValue;
feeIncreasePercentage = _feeIncreasePercentage;
platformFeePercentage = _platformFeePercentage;
// Initialize game state for the first round
claimFee = initialClaimFee;
gracePeriod = initialGracePeriod;
+ pot = initialPotValue;
lastClaimTime = block.timestamp; // Game starts immediately upon deployment
gameRound = 1;
gameEnded = false;
// currentKing starts as address(0) until first claim
}
...
function resetGame() external onlyOwner gameEndedOnly {
currentKing = address(0);
lastClaimTime = block.timestamp;
- pot = 0;
+ pot = initialPotValue;
claimFee = initialClaimFee;
gracePeriod = initialGracePeriod;
gameEnded = false;
gameRound = gameRound + 1;
emit GameReset(gameRound, block.timestamp);
}
+ function updateInitialPotValue(uint256 _newInitialPotValue) external onlyOwner {
+ require(_newInitialPotValue > 0, "Game: New initial pot value must be greater than zero.");
+ initialPotValue = _newInitialPotValue;
+ emit InitialPotValueUpdated(_newInitialPotValue);
+ }