DatingDapp

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

likeUser() locks ETH permanently with no unlike or refund mechanism when a match never occurs

Root + Impact

Description

  • likeUser() requires at least 1 ETH per like. The ETH is sent to LikeRegistry and the likes mapping is set. If the liked user never reciprocates, the ETH is permanently locked in the contract with no way to withdraw.

  • The contract has no unlike() function, no refund() function, and no timeout after which a liker can reclaim their ETH. userBalances is never credited (H-01), so there is no balance to claim even if a withdrawal path existed. The only ETH exit is withdrawFees() (owner only) and the MultiSig transfer in matchRewards.

// src/LikeRegistry.sol — likeUser()
function likeUser(address liked) external payable {
require(msg.value >= 1 ether, "Must send at least 1 ETH");
require(!likes[msg.sender][liked], "Already liked");
...
likes[msg.sender][liked] = true;
// @> msg.value credited nowhere — no refund path if no match
if (likes[liked][msg.sender]) {
matchRewards(liked, msg.sender);
}
// @> function ends — ETH locked if no mutual like
}

Risk

Likelihood:

  • Any user who likes someone and never receives a mutual like permanently loses their ETH. This is the default outcome for most likes on any dating platform.

Impact:

  • All ETH sent via likeUser for non-matching pairs is permanently locked in LikeRegistry. The owner can only withdraw totalFees, which are 10% of matchRewards — not the locked like ETH. Every unreciprocated 1+ ETH like is a permanent loss to the sender.

Proof of Concept

Alice likes Bob and sends 1 ETH. Bob never reciprocates. The test verifies that LikeRegistry holds Alice's ETH indefinitely. The commented-out lines show that no unlike or refund function exists to call — the contract simply has no exit path for this ETH.

function testLikeEthLockedWithoutMatch() public {
// Alice likes Bob — sends 1 ETH
vm.prank(alice);
vm.deal(alice, 1 ether);
likeRegistry.likeUser{value: 1 ether}(bob);
// Bob never likes Alice back
// Alice has no way to retrieve her ETH
assertEq(address(likeRegistry).balance, 1 ether);
// No unlike or refund function exists — ETH is permanently locked
// vm.prank(alice);
// likeRegistry.unlike(bob); // does not exist
// likeRegistry.refund(); // does not exist
}

Alice's 1 ETH sits in LikeRegistry indefinitely with no recovery path.

Recommended Mitigation

Track each liker's outstanding balance and provide a withdrawal function callable after a timeout:

+ uint256 public constant LIKE_EXPIRY = 30 days;
+ mapping(address => mapping(address => uint256)) public likeAmounts;
+ mapping(address => mapping(address => uint256)) public likeTimestamp;
function likeUser(address liked) external payable {
...
likes[msg.sender][liked] = true;
+ likeAmounts[msg.sender][liked] = msg.value;
+ likeTimestamp[msg.sender][liked] = block.timestamp;
...
}
+ function refundLike(address liked) external {
+ require(likes[msg.sender][liked], "No like found");
+ require(!likes[liked][msg.sender], "Already matched");
+ require(block.timestamp >= likeTimestamp[msg.sender][liked] + LIKE_EXPIRY, "Like not expired");
+ uint256 amount = likeAmounts[msg.sender][liked];
+ likeAmounts[msg.sender][liked] = 0;
+ likes[msg.sender][liked] = false;
+ (bool success,) = payable(msg.sender).call{value: amount}("");
+ require(success, "Refund failed");
+ }
Updates

Lead Judging Commences

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