DatingDapp

First Flight #33
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Severity: low
Invalid

No ETH Recovery Mechanism for Unmatched Likes Results in Permanent Fund Lock

Description:
The LikeRegistry contract lacks a mechanism for users to recover ETH sent with likes that never result in matches. Once a user sends ETH with a like, there is no way to withdraw it if the liked user never reciprocates. This creates a permanent lock of user funds, as the ETH remains stuck in the contract indefinitely.

The contract has no functions that allow:

  • Withdrawal of ETH from unmatched likes after a time period

  • Cancellation of existing likes

  • Recovery of funds from likes that will never be matched

Impact:
HIGH - This vulnerability leads to permanent loss of user funds in several scenarios:

  • When liked users never reciprocate

  • If liked users become inactive

  • If liked users lose their private keys

  • If users like the wrong address by mistake

  • If the liked address is a person that cannot/will not like back

The impact is especially severe because:

  1. Each like requires at least 1 ETH

  2. Multiple likes can be sent, multiplying locked funds

  3. There is no time limit on the lock

  4. The funds remain locked even if it becomes certain no match will occur

Proof of Code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../src/LikeRegistry.sol";
import "../src/SoulboundProfileNFT.sol";
import "src/MultiSig.sol";
contract LikeRegistryTest is Test {
LikeRegistry public likeRegistry;
SoulboundProfileNFT public profileNFT;
address alice = address(0x1);
address bob = address(0x2);
address carol = address(0x3);
address charlie = address(0x4);
function setUp() public {
// Deploy ProfileNFT first
profileNFT = new SoulboundProfileNFT();
// Deploy LikeRegistry with ProfileNFT address
likeRegistry = new LikeRegistry(address(profileNFT));
// Mint profile NFTs for test users
vm.startPrank(alice);
profileNFT.mintProfile("Alice", 25, "ipfs://alice");
vm.stopPrank();
vm.startPrank(bob);
profileNFT.mintProfile("Bob", 28, "ipfs://bob");
vm.stopPrank();
vm.startPrank(carol);
profileNFT.mintProfile("Carol", 24, "ipfs://carol");
vm.stopPrank();
// Fund test accounts
vm.deal(alice, 10 ether);
vm.deal(bob, 10 ether);
vm.deal(carol, 10 ether);
}
function testNoETHRecoveryForUnmatchedLikes() public {
// Initial contract balance
uint256 initialBalance = address(likeRegistry).balance;
// Alice likes Bob with 1 ETH
vm.startPrank(alice);
likeRegistry.likeUser{value: 1 ether}(bob);
vm.stopPrank();
// Verify Alice's ETH is locked
assertEq(address(likeRegistry).balance, initialBalance + 1 ether, "Contract should hold Alice's ETH");
// Verify no way to recover ETH
vm.startPrank(alice);
vm.expectRevert();
(bool success,) = address(likeRegistry).call(abi.encodeWithSignature("recoverUnmatchedLike(address)", bob));
assertFalse(success, "Should not be able to recover ETH from unmatched like");
vm.stopPrank();
}

Proof of Concept:
Step-by-step demonstration of funds being permanently locked:

  1. Alice sends a like to Bob with 2 ETH:

vm.startPrank(alice);
likeRegistry.likeUser{value: 2 ether}(bob);
vm.stopPrank();
  1. Bob never likes Alice back (account lost, inactive, etc.)

// No matching like is ever created
// Alice's 2 ETH is now permanently locked
  1. Alice has no way to recover her 2 ETH:

// Contract has no withdrawal function
// No cancellation mechanism
// No time-based recovery
assertEq(address(likeRegistry).balance >= 2 ether, true);
  1. The ETH remains stuck in the contract forever:

// ETH can only leave via:
// - matchRewards() - requires mutual like
// - withdrawFees() - only for fee portion
// No other ETH withdrawal mechanisms exist

Recommended Mitigation:
Add a time-based recovery mechanism for unmatched likes:

struct Like {
address liker;
uint256 amount;
uint256 timestamp;
bool exists;
}
function withdrawUnmatchedLike(address liked) external {
Like storage like = likes[msg.sender][liked];
require(like.exists, "Like does not exist");
require(block.timestamp >= like.timestamp + 30 days, "Wait period not elapsed");
require(!likes[liked][msg.sender], "Like has been matched");
uint256 amount = like.amount;
delete likes[msg.sender][liked];
(bool success,) = payable(msg.sender).call{value: amount}("");
require(success, "Transfer failed");
emit LikeWithdrawn(msg.sender, liked, amount);
}

Additional recommendations:

  1. Add a like cancellation window (e.g., 1 hour) for immediate recovery of mistaken likes

  2. Implement a maximum like duration after which likes expire and ETH can be recovered

  3. Add events for withdrawals and cancellations

  4. Consider adding a fee for early withdrawals to prevent gaming

  5. Add clear documentation about the recovery process

These changes will prevent permanent fund lockup while maintaining the core matching mechanism's integrity.

Updates

Appeal created

n0kto Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

Informational or Gas

Please read the CodeHawks documentation to know which submissions are valid. If you disagree, provide a coded PoC and explain the real likelyhood and the detailed impact on the mainnet without any supposition (if, it could, etc) to prove your point.

Support

FAQs

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