SNARKeling Treasure Hunt

First Flight #59
Beginner FriendlyGameFiFoundry
100 EXP
Submission Details
Impact: medium
Likelihood: low

M-02: receive() accepts ETH from any sender, bypassing the ONLY_OWNER_CAN_FUND restriction

Author Revealed upon completion

Root + Impact

Description

  • fund() enforces that only the owner can add ETH to the contract. The receive() fallback, which handles plain ETH transfers, has no equivalent check and accepts ETH from any address.

function fund() external payable {
// @> owner-only gate
require(msg.sender == owner, "ONLY_OWNER_CAN_FUND");
require(msg.value > 0, "NO_ETH_SENT");
emit Funded(msg.value, address(this).balance);
}
// @> no sender check — anyone can call this via a plain ETH transfer
receive() external payable {
emit Funded(msg.value, address(this).balance);
}

Risk

Likelihood:

  • Any address can send ETH directly to the contract at any time; no privileged access is required.

  • A griefing actor has no financial incentive, so this is less likely to be exploited maliciously than other findings.

Impact:

  • Breaks the owner-only funding invariant; off-chain systems relying on Funded events to track authorized funding will record unauthorized entries.

  • An actor can keep the balance permanently above zero, interfering with any zero-balance logic or monitoring that assumes only the owner adds funds.

Proof of Concept

Any external account can send ETH directly to the contract's address using a plain transfer, bypassing the ONLY_OWNER_CAN_FUND guard entirely. The receive() fallback accepts the value unconditionally and emits a Funded event, making the unauthorized deposit indistinguishable from an owner-initiated one in off-chain monitoring systems.

vm.prank(attacker);
(bool sent,) = address(hunt).call{value: 1 ether}(""); // bypasses ONLY_OWNER_CAN_FUND
assertTrue(sent); // succeeds, Funded event emitted with attacker as sender

Recommended Mitigation

  • Adding a sender check inside receive() enforces the same invariant already expressed in fund(), ensuring that the Funded event is only ever emitted for authorized deposits. Alternatively, removing receive() altogether and directing all ETH intake through fund() eliminates the discrepancy at the cost of requiring callers to use the explicit function signature rather than plain transfers.

  • Or remove receive() entirely and rely solely on fund().

receive() external payable {
+ require(msg.sender == owner, "ONLY_OWNER_CAN_FUND");
emit Funded(msg.value, address(this).balance);
}

Support

FAQs

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

Give us feedback!