Root + Impact
Description
The implementation relies on a single EOA owner with absolute power and no execution delay.
Line 262 require(paused, THE_CONTRACT_MUST_BE_PAUSED);
Line 263 require(msg.sender == owner, ONLY_OWNER_CAN_EMERGENCY_WITHDRAW);
The check recipient != owner is easily bypassed by the owner providing a secondary wallet address they control.
Risk
Likelihood:
-
Reason 1 It requires the owner to act maliciously or for their private key to be compromised.
-
Reason 2 In a competitive audit context, centralization risks are documented even if the owner is currently trusted.
Impact:
-
Impact 1 If exploited, 100 percent of the contract value is lost.
-
Impact 2 Participants who have already spent resources finding treasures lose their rewards permanently.
Proof of Concept
PoC Explanation
The following test script demonstrates how a single owner can bypass the intended emergency nature of the function to perform a total drain of funds. The owner triggers the pause state and then immediately calls emergencyWithdraw to a secondary address. The assert confirms the contract balance is zeroed out instantly.
function test_ownerCentralizationDrain() public {
address attacker = makeAddr(attacker);
uint256 contractBalance = address(treasureHunt).balance;
vm.prank(owner);
treasureHunt.pause();
vm.prank(owner);
treasureHunt.emergencyWithdraw(payable(attacker), contractBalance);
assertEq(address(treasureHunt).balance, 0);
assertEq(attacker.balance, contractBalance);
}
Recommended Mitigation
+ uint256 public constant WITHDRAWAL_DELAY = 2 days;
+ uint256 public pausedAt;
function pause() external onlyOwner {
paused = true;
+ pausedAt = block.timestamp;
}
function emergencyWithdraw(...) external {
require(paused, THE_CONTRACT_MUST_BE_PAUSED);
- require(msg.sender == owner, ONLY_OWNER);
+ require(block.timestamp >= pausedAt + WITHDRAWAL_DELAY, Timelock not expired);
+ require(msg.sender == owner, ONLY_OWNER);
...
}