Last Man Standing

First Flight #45
Beginner FriendlyFoundrySolidity
100 EXP
View results
Submission Details
Impact: medium
Likelihood: low
Invalid

Absence of an initial pot value reduces the incentive for players to become the first claimer, making early participation less appealing

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) { // 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 without initial pot value
claimFee = initialClaimFee;
gracePeriod = initialGracePeriod;
lastClaimTime = block.timestamp;
gameRound = 1;
gameEnded = false;
}

Risk

Likelihood:

  • Occur upon contract deployment and at every new game when resetGame function is called as pot value is set or reset to zero.

Impact:

  • The project could have a difficult launch for every game and potential of low participation due to player hesitation or reluctance to initiate the first claim, stemming from a lack of incentive to be the first mover.

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);
// player1 becomes the first claimer
vm.prank(player1);
// with correction of error done in `claimThrone` function before running the test
// pre-requisite to update this line: require(msg.sender != currentKing, "Game: You are already the king. No need to re-claim.");
game.claimThrone{value: INITIAL_CLAIM_FEE}();
// no other subsequent claimers and grace period is over
vm.warp(game.lastClaimTime() + GRACE_PERIOD + 1 minutes);
game.declareWinner();
// player1 withdraw winning amount
vm.prank(player1);
game.withdrawWinnings();
uint256 player1Balance_after = player1.balance;
console2.log("player1Balance_after: ", player1Balance_after);
// first claimer being the sole winner gets back less than what he owns initially
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);
+ }
Updates

Appeal created

inallhonesty Lead Judge about 2 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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