TreasureHunt::withdraw Enables Unauthorized Users to Trigger Owner Fund Withdrawal, Enabling Griefing AttacksThe missing access control in TreasureHunt::withdraw allows unauthorized (non-owner) users to trigger the fund withdrawal to the owner address after a treasure hunt has finished. The withdraw function is intended to be callable only by the contract owner after the conclusion of the current treasure hunt. The treasure hunt concludes upon the satisfaction of this condition: claimsCount >= MAX_TREASURES. However, the function suffers from lack of adequate access control, such as the onlyOwner modifier or an explicit require(msg.sender == owner). This omission reveals a discrepancy between the documented intent (the NatSpec comment states "Allow the owner to withdraw...") and the actual implementation, which imposes no caller restrictions. As a result, after the hunt ends, any external account can invoke the withdraw function and force the contract's entire ETH balance to be sent to the owner address.
Likelihood:
This issue has a high likelihood of occurring. The withdraw function becomes callable by any address immediately after the final treasure is claimed (claimsCount >= MAX_TREASURES). Invoking this function requires no special permissions, capital, or technical expertise, so any external account can submit the transaction. Additionally, an attacker monitoring the mempool can front‑run the owner's intended withdrawal with a higher gas price, reliably executing the griefing attack. Even without front‑running, any curious or malicious user can call the function at any time after hunt has concluded.
Impact:
While the funds themselves are not stolen, this violates the principle of least privilege and opens the door to operational griefing. The lack of access control in the withdraw function enables a griefing attack with a four-fold impact. One, any user can prematurely force the owner to receive the contract's balance. This may occur at an inconvenient time, disrupting the operational cash flow. Two, if the owner intends to keep the funds in the contract (e.g., for a second treasure hunt), the attacker forces the owner to spend gas to first receive the funds and then re-deposit them via TreasureHunt::fund. An attacker can repeat the process each time the contract is re-funded and a hunt ends, resulting in cumulative gas waste. Three, an attacker monitoring the mempool can front-run the owner's own withdraw transaction. In doing so, the attacker's call succeeds, and the owner's transaction reverts with NO_FUNDS_TO_WITHDRAW, wasting the owner's gas and causing confusion. Four, although these funds ultimately return to the owner, the attack degrades the owner or team's control over their own protocol—and thus eroding trust in the protocol. To summarize, this issue is classified as medium severity because, while not resulting in permanent loss of funds (the owner can recover them), it enables a reliable griefing attack vector that disrupts intended protocol operations as well as imposes unnecessary gas costs to the owner.
The following scenario outlines a realistic griefing attack that does not require any special privileges or ZK proof knowledge:
The attacker runs a simple script (or uses a block explorer) to watch the Claimed events emitted by the TreasureHunt contract. They maintain a counter of how many treasures have been claimed.
When the claimsCount reaches MAX_TREASURES - 1, the attacker prepares a transaction calling withdraw() with a competitive gas price.
As soon as the final claim transaction appears in the mempool, the attacker submits their withdraw() transaction. If it is mined before the owner's intended withdrawal, the attacker successfully empties the contract.
Even without front‑running, the attacker can simply call withdraw() at any moment after the final claim. The owner has no way to prevent this.
The owner receives the ETH (so funds are not lost), however:
If the owner planned to use the remaining balance for another purpose (e.g., a second treasure hunt), they must now spend gas to re‑deposit funds via fund().
If the owner had submitted their own withdraw() transaction, it reverts, wasting that gas.
The owner loses control over the exact timing of fund withdrawal.
The provided Proof of Code simulates this exact scenario using a mock verifier to bypass ZK proof validation, demonstrating that any non‑owner address can successfully call withdraw() after 10 claims.
Insert the following test in TreasureHunt.t.sol.
Before doing so insert the following import statement in the test file.
Place this mock verifier contract into TreasureHunt.t.sol as well.
It is recommended to impose proper access controls on the withdraw function. For code clarity and readability, use the existing onlyOwner modifier as such:
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.
The contest is complete and the rewards are being distributed.