DatingDapp

AI First Flight #6
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Impact: medium
Likelihood: medium
Invalid

Medium: Arbitrary ETH Sent To LikeRegistry Can Become Permanently Locked

Medium: Arbitrary ETH Sent To LikeRegistry Can Become Permanently Locked

Description

  • The contract exposes a payable receive() function allowing arbitrary ETH transfers directly to the contract.

  • However, the protocol only allows withdrawal of tracked totalFees.

  • ETH received outside the expected accounting flow cannot be recovered through any existing function.

  • Users sending ETH directly to the contract or accidentally overpaying may permanently lose funds.

// Root cause in the codebase with @> marks to highlight the relevant section
// @> Accepts arbitrary ETH transfers
receive() external payable {}
function withdrawFees() external onlyOwner {
// @> Only tracked fees can be withdrawn
uint256 totalFeesToWithdraw = totalFees;
totalFees = 0;
(bool success,) = payable(owner()).call{value: totalFeesToWithdraw}("");
}

Risk

Likelihood:

  • Users commonly send ETH directly to contracts accidentally through wallets or block explorers.

  • Any ETH transferred outside fee accounting remains untracked.

Impact:

  • ETH becomes permanently locked inside the protocol.

  • Users may irreversibly lose funds.

  • Contract balance can diverge from protocol accounting assumptions.

Proof of Concept

The following test demonstrates that arbitrary ETH can be sent directly to the contract through the receive() function without being tracked by the protocol accounting system.

First, the test funds a user account with 1 ether using vm.deal().

The user then directly transfers ETH to the LikeRegistry contract using a low-level .call{value: 1 ether}(""), which triggers the payable receive() function.

Because the receive() function accepts ETH without updating any accounting variables such as totalFees, the deposited ETH becomes untracked.

The test then verifies that:

  1. The ETH transfer succeeds.

  2. The contract balance increases by 1 ether.

  3. The owner is unable to withdraw the ETH because withdrawFees() only allows withdrawal of totalFees, which remains 0.

As a result, the transferred ETH becomes permanently locked inside the contract.

function testDirectETHGetsLocked() public {
vm.deal(user, 1 ether);
vm.prank(user);
(bool success,) = address(likeRegistry).call{value: 1 ether}("");
assertTrue(success);
// ETH is now trapped inside contract
assertEq(address(likeRegistry).balance, 1 ether);
// Owner cannot withdraw because totalFees == 0
vm.expectRevert("No fees to withdraw");
likeRegistry.withdrawFees();
}

Recommended Mitigation

The simplest mitigation is to reject arbitrary ETH transfers by reverting inside the receive() function.

This ensures that users can only send ETH through intended protocol flows where accounting and balance tracking are properly handled.

Rejecting direct ETH transfers prevents:

  • Accidental user fund loss.

  • Untracked ETH accumulation.

  • Divergence between actual contract balance and protocol accounting variables.

- receive() external payable {}
+ receive() external payable {
+ revert("Direct ETH transfers disabled");
+ }
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 1 day ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!