DatingDapp

AI First Flight #6
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Severity: high
Valid

Untracked ETH (Like Payments and Direct Transfers) Can Become Permanently Stuck

Untracked ETH (Like Payments and Direct Transfers) Can Become Permanently Stuck


Description

The contract accepts ETH through paid likes (likeUser()) and through direct transfers (receive()).

function likeUser(address liked) external payable { ... }
receive() external payable {}

However, the only withdrawal path is withdrawFees(), and that function transfers only totalFees:

uint256 totalFeesToWithdraw = totalFees;
totalFees = 0;
(bool success,) = payable(owner()).call{value: totalFeesToWithdraw}("");

There is no function that withdraws address(this).balance, and there is no complete accounting path that maps all incoming ETH to totalFees. In the current logic, ETH paid in likeUser() is not added to totalFees, and ETH received through receive() is not added either. As a consequence, contract ETH can accumulate outside the withdrawable fee bucket and become permanently unreachable.


Risk

Likelihood: High

The issue follows normal protocol behavior (users paying for likes) and does not require edge conditions or privileged access.

Impact: High

User ETH and externally sent ETH can become permanently locked, causing irreversible value loss and significant operational and reputational damage.


Proof of Concept

Deterministic scenario:

  1. User calls likeUser() and pays 1 ETH.

  2. Contract balance increases by 1 ETH.

  3. totalFees does not increase from that payment path.

  4. Owner calls withdrawFees() and can withdraw only the totalFees amount.

  5. Remaining ETH (from likes/direct sends) has no dedicated withdrawal path and can remain stuck indefinitely.


Recommended Mitigation

Define one coherent accounting model and align all inflows and outflows to that model.

Minimum safe approach:

  • Account like payments either to user escrow (userBalances) or to fee pool (totalFees) according to intended economics.

  • Restrict or remove raw receive() if unsolicited ETH is not intended.

  • Add an explicit recovery path for unexpected ETH with strict access control and event logging.

Illustrative addition for accidental ETH recovery:

+event ExcessEthRecovered(address indexed to, uint256 amount);
+
+function recoverExcessEth(uint256 amount) external onlyOwner {
+ require(amount <= address(this).balance, "Insufficient balance");
+ (bool success,) = payable(owner()).call{value: amount}("");
+ require(success, "Transfer failed");
+ emit ExcessEthRecovered(owner(), amount);
+}

If user funds are intended to be non-custodial and match-bound, replace owner recovery with explicit user-facing refund/claim mechanics.

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 5 hours ago
Submission Judgement Published
Validated
Assigned finding tags:

[H-01] After the user calls the `likeUser` function, the userBalance does not increase by the corresponding value.

## Description User A calls `likeUser` and sends `value > 1` ETH. According to the design of DatingDapp, the amount for user A should be accumulated by `userBalances`. Otherwise, in the subsequent calculations, the balance for each user will be 0. ## Vulnerability Details When User A calls `likeUser`, the accumulation of `userBalances` is not performed. ```solidity function likeUser( address liked ) external payable { require(msg.value >= 1 ether, "Must send at least 1 ETH"); require(!likes[msg.sender][liked], "Already liked"); require(msg.sender != liked, "Cannot like yourself"); require(profileNFT.profileToToken(msg.sender) != 0, "Must have a profile NFT"); require(profileNFT.profileToToken(liked) != 0, "Liked user must have a profile NFT"); likes[msg.sender][liked] = true; emit Liked(msg.sender, liked); // Check if mutual like if (likes[liked][msg.sender]) { matches[msg.sender].push(liked); matches[liked].push(msg.sender); emit Matched(msg.sender, liked); matchRewards(liked, msg.sender); } } ``` This will result in `totalRewards` always being 0, affecting all subsequent calculations: ```solidity uint256 totalRewards = matchUserOne + matchUserTwo; uint256 matchingFees = (totalRewards * FIXEDFEE ) / 100; uint256 rewards = totalRewards - matchingFees; totalFees += matchingFees; ``` ## POC ```solidity function testUserBalanceshouldIncreaseAfterLike() public { vm.prank(user1); likeRegistry.likeUser{value: 20 ether}(user2); assertEq(likeRegistry.userBalances(user1), 20 ether, "User1 balance should be 20 ether"); } ``` Then we will get an error: ```shell [FAIL: User1 balance should be 20 ether: 0 != 20000000000000000000] ``` ## Impact - Users will be unable to receive rewards. - The contract owner will also be unable to withdraw ETH from the contract. ## Recommendations Add processing for `userBalances` in the `likeUser` function: ```diff function likeUser( address liked ) external payable { require(msg.value >= 1 ether, "Must send at least 1 ETH"); require(!likes[msg.sender][liked], "Already liked"); require(msg.sender != liked, "Cannot like yourself"); require(profileNFT.profileToToken(msg.sender) != 0, "Must have a profile NFT"); require(profileNFT.profileToToken(liked) != 0, "Liked user must have a profile NFT"); likes[msg.sender][liked] = true; + userBalances[msg.sender] += msg.value; emit Liked(msg.sender, liked); [...] } ```

Support

FAQs

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

Give us feedback!