Last Man Standing

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

Contract Can Be Permanently Bricked by renounceOwnership()

Root + Impact

Description

  • Normal behavior:
    Admin functions should remain accessible to manage the game across multiple rounds, and platform fees should be withdrawable by the owner.

    Specific issue:
    The contract inherits OpenZeppelin's Ownable without overriding renounceOwnership(). This function allows the owner to permanently set ownership to address(0), making all onlyOwner functions permanently inaccessible and locking platform fees forever.

// src/Game.sol
contract Game is Ownable {
// @> Inherits renounceOwnership() without override
function resetGame() external onlyOwner gameEndedOnly { /* becomes inaccessible */ }
function updateGracePeriod(uint256 _newGracePeriod) external onlyOwner { /* becomes inaccessible */ }
function updateClaimFeeParameters(...) external onlyOwner { /* becomes inaccessible */ }
function updatePlatformFeePercentage(uint256 _newPercentage) external onlyOwner { /* becomes inaccessible */ }
function withdrawPlatformFees() external onlyOwner { /* becomes inaccessible */ }
}

Risk

Likelihood:

  • Owner accidentally calls renounceOwnership() thinking it's safe.

  • Compromised owner key used maliciously.

  • Social engineering attacks targeting the owner.

Impact:

  • All admin functions become permanently inaccessible.

  • Game cannot be reset after the first round ends.

  • Platform fees are locked forever with no recovery mechanism.

  • Contract becomes a "one-time-use" game that cannot restart.

  • Users lose access to the game permanently.

Proof of Concept

The following tests demonstrate the catastrophic impact of calling renounceOwnership():

function testRenounceOwnershipBricksContract() public {
// Player claims throne and accumulates platform fees
game.claimThrone{value: INITIAL_CLAIM_FEE}();
assertTrue(game.platformFeesBalance() > 0, "Platform fees should be accumulated");
// End the game
vm.warp(block.timestamp + GRACE_PERIOD + 1);
game.declareWinner();
// Owner renounces ownership - BRICKS THE CONTRACT
vm.startPrank(deployer);
game.renounceOwnership();
vm.stopPrank();
// Verify owner is now address(0)
assertEq(game.owner(), address(0), "Owner should be address(0) after renunciation");
// ALL admin functions are now permanently broken
vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(this)));
game.resetGame(); // Can't start new rounds
vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(this)));
game.withdrawPlatformFees(); // Platform fees locked forever
}
function testPlatformFeesLockedForever() public {
// Accumulate platform fees from multiple claims
game.claimThrone{value: INITIAL_CLAIM_FEE}();
address player2 = makeAddr("player2");
vm.deal(player2, 10 ether);
vm.startPrank(player2);
game.claimThrone{value: game.claimFee()}();
vm.stopPrank();
uint256 platformFeesBeforeRenounce = game.platformFeesBalance();
assertTrue(platformFeesBeforeRenounce > 0, "Platform fees should be substantial");
// Owner renounces ownership
vm.startPrank(deployer);
game.renounceOwnership();
vm.stopPrank();
// Platform fees still exist but can never be withdrawn
assertEq(game.platformFeesBalance(), platformFeesBeforeRenounce, "Platform fees still exist");
vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(this)));
game.withdrawPlatformFees(); // ETH is permanently lost
}

Recommended Mitigation

Override renounceOwnership() to prevent ownership renunciation and protect the contract from being permanently bricked:

+ function renounceOwnership() public pure override {
+ revert("Game: Ownership cannot be renounced");
+ }
Updates

Appeal created

inallhonesty Lead Judge 11 days ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
r4y4n3 Submitter
11 days ago
inallhonesty Lead Judge
10 days ago
inallhonesty Lead Judge 6 days ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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