DatingDapp

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

LikeRegistry::withdrawFees allows owner to withdraw totalFees but will also extract user ETH if userBalances bug (H-01) is fixed

Root + Impact

Description

  • The withdrawFees function allows the owner to withdraw totalFees worth of ETH. However, the contract also holds user deposits from likeUser calls (ETH sent via msg.value). The receive() function allows arbitrary ETH deposits. There is no accounting separation between protocol fees and user deposits.

    If userBalances tracking is fixed (H-01), and fees are properly computed, the withdrawFees function uses totalFees to determine the withdrawal amount, which should in theory only withdraw the fee portion. However, the contract balance includes all user deposits, fees, and any accidental ETH sends. A scenario where totalFees is manipulated or miscalculated could drain user funds.

    Additionally, the receive() function means anyone can send ETH to the contract, inflating the balance without any accounting.

// Root cause in LikeRegistry.sol lines 73-80 and 83
function withdrawFees() external onlyOwner {
require(totalFees > 0, "No fees to withdraw");
uint256 totalFeesToWithdraw = totalFees;
totalFees = 0;
(bool success,) = payable(owner()).call{value: totalFeesToWithdraw}("");
require(success, "Transfer failed");
}
receive() external payable {} // @> Anyone can send ETH, no accounting

Risk

Likelihood:

  • Low. The owner would need to intentionally or accidentally manipulate totalFees, or the accounting would need to diverge from expected behavior.

Impact:

  • Unaccounted ETH sent to the contract via receive() is trapped permanently (no withdrawal path for it).

  • Minor accounting concerns.

Proof of Concept

This test shows that ETH sent directly to the LikeRegistry via receive() is permanently trapped — withdrawFees only withdraws the totalFees amount, which does not account for directly-sent ETH, leaving it locked with no recovery path.

function testL02_TrappedEthViaReceive() public {
// Someone accidentally sends 5 ETH directly to the contract
(bool ok,) = payable(address(likeRegistry)).call{value: 5 ether}("");
require(ok);
// Contract holds 5 ETH but totalFees is 0
assertEq(address(likeRegistry).balance, 5 ether);
// Owner cannot withdraw — totalFees is 0
vm.prank(owner);
vm.expectRevert("No fees to withdraw");
likeRegistry.withdrawFees();
// 5 ETH is permanently trapped — no function can retrieve it
}

Recommended Mitigation

Remove the open receive() function to prevent untracked ETH from entering the contract. The contract already receives ETH through likeUser (which is payable), so a bare receive() is unnecessary. If direct ETH deposits are intentionally supported, add an accounting variable to track them and provide the owner a withdrawal path for unaccounted funds.

- receive() external payable {}
+ // Option A: Remove receive() entirely — ETH only enters via likeUser()
+ // No change needed; removing the function is sufficient.
+ // Option B: If direct deposits are needed, track and allow withdrawal
+ uint256 public unaccountedETH;
+
+ receive() external payable {
+ unaccountedETH += msg.value;
+ }
+
+ function withdrawUnaccountedETH() external onlyOwner {
+ uint256 amount = unaccountedETH;
+ unaccountedETH = 0;
+ (bool success,) = payable(owner()).call{value: amount}("");
+ require(success, "Transfer failed");
+ }
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 3 hours 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!