SNARKeling Treasure Hunt

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

(LOW) `receive()` is public and unrestricted, completely bypassing the `OnlyOwnerCanFund` check on `fund()`

Location: contracts/src/TreasureHunt.sol:236-241 (fund) vs contracts/src/TreasureHunt.sol:287-289 (receive)

Description

fund() is owner-only:

function fund() external payable {
require(msg.sender==owner, "ONLY_OWNER_CAN_FUND");
require(msg.value > 0, "NO_ETH_SENT");
emit Funded(msg.value, address(this).balance);
}

But receive() accepts ETH from anyone and emits the same Funded event:

receive() external payable {
emit Funded(msg.value, address(this).balance);
}

A non-owner can send ETH directly to the contract address, deposit any amount, and trigger a Funded event bypassing the ONLY_OWNER_CAN_FUND restriction.

Risk

Likelihood: High. Very easy to transfer ETH directly.

Impact: Low. No one can steal but anyone can send ETH to the contract.

Proof of Concept

function test_ReceiveBypassesOnlyOwnerFund() public {
address stranger = makeAddr("stranger");
vm.deal(stranger, 5 ether);
// fund() requires owner:
vm.prank(stranger);
vm.expectRevert("ONLY_OWNER_CAN_FUND");
hunt.fund{value: 1 ether}();
// But receive() does not:
uint256 balBefore = address(hunt).balance;
vm.prank(stranger);
(bool ok, ) = address(hunt).call{value: 1 ether}("");
assertTrue(ok);
assertEq(address(hunt).balance, balBefore + 1 ether);
}

Run:

forge test --match-test test_ReceiveBypassesOnlyOwnerFund -vv

The test passes stranger's fund() call reverts, but their direct ETH transfer succeeds.

Recommended Mitigation

Either restrict receive() with the same owner check, or delete it entirely and force all deposits through fund().

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

Or:

- receive() external payable {
- 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!