SNARKeling Treasure Hunt

First Flight #59
Beginner FriendlyGameFiFoundry
100 EXP
View results
Submission Details
Severity: low
Valid

withdraw() Missing Access Control Anyone Can Trigger Post-Hunt Withdrawal

Root + Impact

Description

  • The documented design states withdrawal of leftover funds is an owner-controlled admin flow.

  • withdraw() sends the entire contract balance to owner, which prevents direct theft, but the function lacks the onlyOwner modifier, allowing any external account to invoke it.

// Root cause in the codebase with @> mark// @> No onlyOwner modifier — any caller can trigger this
function withdraw() external {
require(claimsCount >= MAX_TREASURES, "HUNT_NOT_OVER");
uint256 balance = address(this).balance;
require(balance > 0, "NO_FUNDS_TO_WITHDRAW");
// @> Always sends to owner, so no theft — but caller is not authenticated
(bool sent, ) = owner.call{value: balance}("");
require(sent, "ETH_TRANSFER_FAILED");
emit Withdrawn(balance, address(this).balance);
}s to highlight the relevant section

Risk

Likelihood:

  • Any on-chain observer can call this function once claimsCount == MAX_TREASURES.

Bots routinely front-run state-change events to trigger permissionless functions.

Impact:

  • The owner loses control over the timing of their own withdrawal — a third party forces the transfer at any moment after the hunt ends.

  • Gas refund griefing: a malicious actor can repeatedly front-run the owner's own withdrawal transaction.

  • Violates the principle of least privilege and the protocol's documented access control model.

Proof of Concept

After all 10 treasures are claimed:

Attacker calls withdraw() before owner does.

hunt.withdraw(); // Succeeds — sends balance to owner despite attacker calling

Recommended Mitigation

Add the onlyOwner modifier:

- remove this function withdraw() external onlyOwner {
require(claimsCount >= MAX_TREASURES, "HUNT_NOT_OVER");
...
}
Updates

Lead Judging Commences

s3mvl4d Lead Judge 18 days ago
Submission Judgement Published
Validated
Assigned finding tags:

broken access control in withdraw()

The `withdraw()` function is intended as an owner-only post-hunt recovery function, but the implementation does not actually enforce any ownership check before transferring the full remaining balance to owner. The function only requires that `claimsCount >= MAX_TREASURES` and that the contract balance is nonzero, after which it sends all ETH to the stored owner address regardless of who called the function. Therefore, the access control on the function itself is incomplete because any external account can trigger the withdrawal path once the hunt is considered over.

Support

FAQs

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

Give us feedback!