DatingDapp

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

Unmatched likes can lock user funds indefinitely

Severity

Medium

Likelihood

High

Root + Impact

Paid likes have no cancellation or refund path, so unmatched user funds can remain locked indefinitely.

Description

  • The expected behavior is that once a user pays to like another profile, the deposited ETH should either participate in a completed match flow or remain recoverable if the match never happens.

  • Instead, the protocol accepts ETH for one-sided likes but provides no cancellation, timeout, or refund mechanism when the counterparty never reciprocates. As a result, funds tied to an unmatched like can remain trapped in the protocol indefinitely.

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");
@> userBalances[msg.sender] += msg.value;
likes[msg.sender][liked] = true;
emit Liked(msg.sender, liked);
if (likes[liked][msg.sender]) {
matches[msg.sender].push(liked);
matches[liked].push(msg.sender);
emit Matched(msg.sender, liked);
matchRewards(liked, msg.sender);
}
}
@> // No function exists to cancel an unmatched like or recover the deposited ETH.

Risk

Likelihood: High

  • This occurs during the normal protocol flow whenever a user pays to like another profile and the counterparty never sends a reciprocal like.

  • The condition can persist indefinitely because the protocol does not provide any user-accessible resolution path for unmatched deposits.

Impact: Medium

  • User funds can remain locked for an unlimited duration without being matched, refunded, or otherwise recoverable.

  • This disrupts the protocol's paid-like flow by allowing normal user deposits to become economically stranded.

Proof of Concept

The following test shows that after wallet_A pays to like wallet_B, the deposited ETH remains recorded for wallet_A, but no function exists to cancel the unmatched like or recover the funds when wallet_B never reciprocates.

function test_unmatchedLike_canLockFundsIndefinitely() public {
address wallet_A = makeAddr("Wallet_A");
address wallet_B = makeAddr("Wallet_B");
deal(wallet_A, 1 ether);
vm.prank(wallet_A);
soulboundNFT.mintProfile("Wallet_A", 25, "ipfs://Wallet_A");
vm.prank(wallet_B);
soulboundNFT.mintProfile("Wallet_B", 35, "ipfs://Wallet_B");
// wallet_A pays to like wallet_B.
vm.prank(wallet_A);
registry.likeUser{value: 1 ether}(wallet_B);
// The funds remain attributed to wallet_A while no match occurs.
assertEq(registry.userBalances(wallet_A), 1 ether);
assertFalse(registry.likes(wallet_B, wallet_A));
// No user-facing function exists to cancel the like or withdraw the pending ETH.
}

Recommended Mitigation

Allow users to cancel an unmatched like and recover the fixed 1 ether deposit tied to that like before a mutual match occurs.

+ function cancelLike(address liked) external {
+ require(likes[msg.sender][liked], "Like does not exist");
+ require(!likes[liked][msg.sender], "Already matched");
+
+ likes[msg.sender][liked] = false;
+ userBalances[msg.sender] -= 1 ether;
+
+ (bool success,) = payable(msg.sender).call{value: 1 ether}("");
+ require(success, "Refund failed");
+ }
Updates

Lead Judging Commences

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