Last Man Standing

First Flight #45
Beginner FriendlyFoundrySolidity
100 EXP
View results
Submission Details
Severity: medium
Valid

[M-1] Previous King Payout Not Implemented (Protocol Deviation)

Description

The claimThrone() function fails to distribute rewards to the previous king as specified in the NatSpec comments:

/**
* @dev If there's a previous king, a small portion of the new claim fee is sent to them.
*/

Key issues:

  1. previousKingPayout is hardcoded to 0

  2. No funds are transferred to the previous king

  3. Pot calculation doesn't account for king's share

Risk

Likelihood:

  • Occurs on every throne claim after the first one

  • Permanently affects all game rounds where multiple claims occur

Impact:

  • Violates promised game mechanics and economic model

  • Disincentivizes players from becoming king

  • Unfairly advantages subsequent claimants

  • Reduces protocol's trustworthiness

Proof of Concept

  1. Test Setup:

    • Two players with 10 ETH each

    • First claim makes king1 the king (0.1 ETH fee)

    • Second claim should pay king1

  2. Key Verification:

    assertEq(king1.balance, king1BalanceBefore); // No payout received
    assertEq(pot, potBefore + (claimFee - platformFee)); // Full amount goes to pot
  3. What Test Shows:

    • Platform fee works (5%)

    • Missing previous king's payout

    • Pot gets 95% instead of receiving after the cut of previous king

function testMissingPreviousKingPayout() public {
address king1 = makeAddr("king1");
address king2 = makeAddr("king2");
vm.deal(king1, 10 ether);
vm.deal(king2, 10 ether);
// First claim - king1 becomes king
uint256 firstClaimFee = game.claimFee();
vm.prank(king1);
game.claimThrone{value: firstClaimFee}();
// Get balances before second claim
uint256 king1BalanceBefore = king1.balance;
uint256 potBefore = game.pot();
uint256 platformFeesBefore = game.platformFeesBalance();
// Second claim - king2 claims throne
uint256 secondClaimFee = game.claimFee();
vm.prank(king2);
game.claimThrone{value: secondClaimFee}();
// Verify NO payout to previous king (current behavior)
assertEq(
king1.balance, king1BalanceBefore, "Previous king should not receive payout (as currently implemented)"
);
// Verify platform fee was taken (5%)
uint256 expectedPlatformFee = (secondClaimFee * game.platformFeePercentage()) / 100;
assertEq(
game.platformFeesBalance(), platformFeesBefore + expectedPlatformFee, "Platform fee not properly collected"
);
// Verify pot received remaining 95%
assertEq(
game.pot(),
potBefore + (secondClaimFee - expectedPlatformFee),
"Pot should receive claim amount minus platform fee"
);
}

Logs

forge test --mt testMissingPreviousKingPayout -vvvv
[⠊] Compiling...
No files changed, compilation skipped
Ran 1 test for test/Game.t.sol:GameTest
[PASS] testMissingPreviousKingPayout() (gas: 201323)
Traces:
[241123] GameTest::testMissingPreviousKingPayout()
├─ [0] VM::addr(<pk>) [staticcall]
│ └─ ← [Return] king1: [0x273db53DB5E931138041D5FE526b5a72C915f359]
├─ [0] VM::label(king1: [0x273db53DB5E931138041D5FE526b5a72C915f359], "king1")
│ └─ ← [Return]
├─ [0] VM::addr(<pk>) [staticcall]
│ └─ ← [Return] king2: [0xB47db2FFB3E995c994169A28730C3881530bDaF2]
├─ [0] VM::label(king2: [0xB47db2FFB3E995c994169A28730C3881530bDaF2], "king2")
│ └─ ← [Return]
├─ [0] VM::deal(king1: [0x273db53DB5E931138041D5FE526b5a72C915f359], 10000000000000000000 [1e19])
│ └─ ← [Return]
├─ [0] VM::deal(king2: [0xB47db2FFB3E995c994169A28730C3881530bDaF2], 10000000000000000000 [1e19])
│ └─ ← [Return]
├─ [2514] Game::claimFee() [staticcall]
│ └─ ← [Return] 100000000000000000 [1e17]
├─ [0] VM::prank(king1: [0x273db53DB5E931138041D5FE526b5a72C915f359])
│ └─ ← [Return]
├─ [150601] Game::claimThrone{value: 100000000000000000}()
│ ├─ emit ThroneClaimed(newKing: king1: [0x273db53DB5E931138041D5FE526b5a72C915f359], claimAmount: 100000000000000000 [1e17], newClaimFee: 110000000000000000 [1.1e17], newPot: 95000000000000000 [9.5e16], timestamp: 1)
│ └─ ← [Stop]
├─ [515] Game::pot() [staticcall]
│ └─ ← [Return] 95000000000000000 [9.5e16]
├─ [472] Game::platformFeesBalance() [staticcall]
│ └─ ← [Return] 5000000000000000 [5e15]
├─ [514] Game::claimFee() [staticcall]
│ └─ ← [Return] 110000000000000000 [1.1e17]
├─ [0] VM::prank(king2: [0xB47db2FFB3E995c994169A28730C3881530bDaF2])
│ └─ ← [Return]
├─ [50101] Game::claimThrone{value: 110000000000000000}()
│ ├─ emit ThroneClaimed(newKing: king2: [0xB47db2FFB3E995c994169A28730C3881530bDaF2], claimAmount: 110000000000000000 [1.1e17], newClaimFee: 121000000000000000 [1.21e17], newPot: 199500000000000000 [1.995e17], timestamp: 1)
│ └─ ← [Stop]
├─ [536] Game::platformFeePercentage() [staticcall]
│ └─ ← [Return] 5
├─ [472] Game::platformFeesBalance() [staticcall]
│ └─ ← [Return] 10500000000000000 [1.05e16]
├─ [515] Game::pot() [staticcall]
│ └─ ← [Return] 199500000000000000 [1.995e17]
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.31ms (288.61µs CPU time)
Ran 1 test suite in 5.72ms (1.31ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Recommended Mitigation

function claimThrone() external payable gameNotEnded nonReentrant {
require(msg.value >= claimFee);
require(msg.sender != currentKing);
uint256 sentAmount = msg.value;
+ uint256 previousKingPayout = 0;
+
+ if (currentKing != address(0)) {
+ previousKingPayout = (sentAmount * 10) / 100; // Example: 10% to previous king
+ payable(currentKing).transfer(previousKingPayout);
+ }
uint256 currentPlatformFee = (sentAmount * platformFeePercentage) / 100;
// Update calculations to account for king's share
- uint256 amountToPot = sentAmount - currentPlatformFee;
+ uint256 amountToPot = sentAmount - currentPlatformFee - previousKingPayout;
platformFeesBalance += currentPlatformFee;
pot += amountToPot;
// State updates...
}

Key Points:

  1. Only pays when the previous king exists

  2. Transfers 10%(just for example) to previous king

  3. Adjusts pot amount accordingly

Updates

Appeal created

inallhonesty Lead Judge about 2 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Missing Previous King Payout Functionality

Support

FAQs

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