Centralized Control Allows Owner to Manipulate Live Game Dynamics
Description
The contract owner possesses unilateral and immediate power to change critical game parameters—gracePeriod
, initialClaimFee
, feeIncreasePercentage
, and platformFeePercentage
—at any time by calling the respective update functions (updateGracePeriod()
, updateClaimFeeParameters()
, etc.). These functions lack any access control modifier to prevent their execution during an active game round. This introduces a significant centralization risk, as a malicious or compromised owner can alter the game's rules mid-play to benefit themselves or specific players.
@> function updateGracePeriod(uint256 _newGracePeriod) external onlyOwner {
require(_newGracePeriod > 0, "Game: New grace period must be greater than zero.");
gracePeriod = _newGracePeriod;
emit GracePeriodUpdated(_newGracePeriod);
}
@> function updateClaimFeeParameters(
uint256 _newInitialClaimFee,
uint256 _newFeeIncreasePercentage
) external onlyOwner isValidPercentage(_newFeeIncreasePercentage) {
require(_newInitialClaimFee > 0, "Game: New initial claim fee must be greater than zero.");
initialClaimFee = _newInitialClaimFee;
feeIncreasePercentage = _newFeeIncreasePercentage;
emit ClaimFeeParametersUpdated(_newInitialClaimFee, _newFeeIncreasePercentage);
}
@> function updatePlatformFeePercentage(uint256 _newPlatformFeePercentage)
external
onlyOwner
isValidPercentage(_newPlatformFeePercentage)
{
platformFeePercentage = _newPlatformFeePercentage;
emit PlatformFeePercentageUpdated(_newPlatformFeePercentage);
}
Risk
Likelihood:
-
The privileged functions can be executed in a single transaction by the owner at any moment. There are no technical barriers, such as a time-lock or a game state check, preventing their use during a live round.
Impact:
Proof of Concept
The ability for the owner to manipulate game outcomes can be demonstrated with a Foundry test. The following test function was added to the test/Game.t.sol
file to simulate an owner ensuring their friend wins the game.
function testOwnerCanManipulateGracePeriodMidGame() public{
vm.prank(player1);
game.claimThrone{value: INITIAL_CLAIM_FEE}();
assertEq(game.currentKing(),player1,"Player 1 should be the king");
uint256 newClaimFee = game.claimFee();
vm.prank(player2);
game.claimThrone{value: newClaimFee}();
assertEq(game.currentKing(), player2, "Player 2 should now be the king");
assertFalse(game.gameEnded(),"Game should be still be active");
vm.prank(deployer);
uint256 maliciousGracePeriod = 1;
game.updateGracePeriod(maliciousGracePeriod);
assertEq(game.gracePeriod(),maliciousGracePeriod,"Grace period should be update to 1 second");
vm.warp(block.timestamp + 2);
vm.prank(deployer);
game.declareWinner();
assertTrue(game.gameEnded(),"Game should have ended");
uint256 platformFeeFromP1 = (INITIAL_CLAIM_FEE * PLATFORM_FEE_PERCENTAGE) / 100;
uint256 potAfterP1 = INITIAL_CLAIM_FEE - platformFeeFromP1;
uint256 platformFeeFromP2 = (newClaimFee * PLATFORM_FEE_PERCENTAGE) / 100;
uint256 potFromP2 = newClaimFee - platformFeeFromP2;
uint256 expectedPot = potAfterP1 + potFromP2;
assertEq(game.pendingWinnings(player2), expectedPot, "Player 2 should have winnings pending");
assertEq(game.pendingWinnings(player1), 0, "Player 1 should have no winnings");
}
Recommended Mitigation
Restrict parameter updates so they can only occur when a game round is not active. This can be achieved by adding the gameEndedOnly
modifier to the parameter update functions. This ensures that changes can only be made after a winner is declared but before a new game starts.
- function updateGracePeriod(uint256 _newGracePeriod) external onlyOwner {
+ function updateGracePeriod(uint256 _newGracePeriod) external onlyOwner gameEndedOnly {
require(_newGracePeriod > 0, "Game: New grace period must be greater than zero.");
gracePeriod = _newGracePeriod;
emit GracePeriodUpdated(_newGracePeriod);
}
- function updateClaimFeeParameters(
+ function updateClaimFeeParameters(
uint256 _newInitialClaimFee,
uint256 _newFeeIncreasePercentage
- ) external onlyOwner isValidPercentage(_newFeeIncreasePercentage) {
+ ) external onlyOwner gameEndedOnly isValidPercentage(_newFeeIncreasePercentage) {
require(_newInitialClaimFee > 0, "Game: New initial claim fee must be greater than zero.");
initialClaimFee = _newInitialClaimFee;
feeIncreasePercentage = _newFeeIncreasePercentage;
emit ClaimFeeParametersUpdated(_newInitialClaimFee, _newFeeIncreasePercentage);
}
- function updatePlatformFeePercentage(uint256 _newPlatformFeePercentage)
- external
- onlyOwner
- isValidPercentage(_newPlatformFeePercentage)
+ function updatePlatformFeePercentage(uint256 _newPlatformFeePercentage) external onlyOwner gameEndedOnly isValidPercentage(_newPlatformFeePercentage)
{
platformFeePercentage = _newPlatformFeePercentage;
emit PlatformFeePercentageUpdated(_newPlatformFeePercentage);
}