DatingDapp

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

Missing userBalances credit in likeUser() causes match rewards to always distribute zero ETH

Root + Impact

Description

  • LikeRegistry.likeUser() collects 1 ETH from each caller and, when a mutual like is detected, calls matchRewards() to split the pooled payments between the matched couple via a newly deployed MultiSigWallet.

  • likeUser never writes userBalances[msg.sender] += msg.value, so matchRewards reads zero for both participants every time it fires. All ETH paid accumulates permanently in the contract with no recovery path.

function likeUser(address liked) external payable {
require(msg.value >= 1 ether, "Must send at least 1 ETH");
require(profileNFT.profileToToken(msg.sender) != 0, "Must have a profile NFT");
require(profileNFT.profileToToken(liked) != 0, "Liked user must have a profile NFT");
require(!likes[msg.sender][liked], "Already liked");
@> likes[msg.sender][liked] = true;
// userBalances[msg.sender] += msg.value is NEVER written here
emit Liked(msg.sender, liked);
if (likes[liked][msg.sender]) {
matches[msg.sender].push(liked);
matches[liked].push(msg.sender);
@> matchRewards(liked, msg.sender);
}
}
function matchRewards(address from, address to) internal {
@> uint256 matchUserOne = userBalances[from]; // always 0
@> uint256 matchUserTwo = userBalances[to]; // always 0
userBalances[from] = 0;
userBalances[to] = 0;
uint256 totalRewards = matchUserOne + matchUserTwo; // always 0
uint256 matchingFees = (totalRewards * FIXEDFEE) / 100; // always 0
uint256 rewards = totalRewards - matchingFees; // always 0
totalFees += matchingFees;
MultiSigWallet multiSigWallet = new MultiSigWallet(from, to);
@> (bool success,) = payable(address(multiSigWallet)).call{value: rewards}(""); // sends 0
require(success, "Transfer failed");
}

Risk

Likelihood:

  • Every call to likeUser skips the balance write — the bug triggers on 100% of likes without exception.

  • The missing line is structural, not conditional; there is no code path in the current implementation that credits userBalances.

Impact:

  • All ETH paid by liking users (minimum 1 ETH each, 2 ETH per match) is permanently locked in LikeRegistry with no recovery path.

  • Every matched couple receives an empty multisig wallet, completely breaking the protocol's core value proposition of pooling payments for matched users.

  • withdrawFees only drains totalFees, which is also always zero due to the same root cause, so the owner cannot recover the locked ETH either.

Proof of Concept

Place this test in test/ and run forge test --match-test test_matchRewardsAlwaysZero. The test demonstrates that matched users always receive zero ETH from the MultiSig because likeUser() never credits userBalances[msg.sender] with the deposited ETH.

// Attack scenario:
// 1. Alice calls likeUser(Bob) with 1 ETH. userBalances[Alice] stays 0.
// 2. Bob calls likeUser(Alice) with 1 ETH. userBalances[Bob] stays 0.
// 3. matchRewards fires: totalRewards = 0, MultiSig receives 0 ETH.
// 4. 2 ETH is permanently stuck in LikeRegistry.
function test_matchRewardsAlwaysZero() public {
vm.deal(alice, 1 ether);
vm.deal(bob, 1 ether);
vm.prank(alice);
likeRegistry.likeUser{value: 1 ether}(bob);
vm.prank(bob);
likeRegistry.likeUser{value: 1 ether}(alice);
// Contract holds 2 ETH but matched MultiSig received 0
assertEq(address(likeRegistry).balance, 2 ether);
// MultiSig balance is 0 — match reward never paid out
}

Recommended Mitigation

Add userBalances[msg.sender] += msg.value immediately after likes[msg.sender][liked] = true so every liker's balance reflects their deposited ETH before the mutual-like check fires.

likes[msg.sender][liked] = true;
+ userBalances[msg.sender] += msg.value;
emit Liked(msg.sender, liked);
if (likes[liked][msg.sender]) {
matches[msg.sender].push(liked);
matches[liked].push(msg.sender);
matchRewards(liked, msg.sender);
}
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!