SNARKeling Treasure Hunt

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

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

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);
}
Updates

Lead Judging Commences

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

nonowner can fund

Although the explicit `fund()` function is restricted to the owner via `require(msg.sender == owner, "ONLY_OWNER_CAN_FUND")`, the contract also implements a permissive `receive()` function that accepts arbitrary ETH sent directly to the contract and emits the same Funded event without any sender check. This means a non-owner cannot use the `fund()` entrypoint itself, but can still increase the contract balance simply by transferring ETH to the contract address. This is an access-control inconsistency between the documented admin funding path and the actual fallback behavior.

Support

FAQs

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

Give us feedback!