DatingDapp

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

likeUser accepts ETH but never updates userBalances, causing all match rewards to be zero and user funds to be permanently locked

Root + Impact

Description

  • likeUser should record the caller's ETH in userBalances[msg.sender] so matchRewards can pool it into the multisig on a mutual match

  • likeUser accepts msg.value but never writes to userBalances, so every match reads zero, funds the multisig with 0 ETH, and traps user funds in the contract

Risk

Likelihood:

  • Occurs on every single call to likeUser, no attacker, no special condition, no edge case required.

  • Every user who sends ETH to like another user is affected, and every match that occurs will produce a zero-funded multisig.

Impact:

  • All ETH sent through `likeUser` is permanently locked in the LikeRegistry contract with no user-callable recovery function. `matchRewards` is the only path that pays regular users, and it always sends 0 ETH because it reads from an empty `userBalances`. `withdrawFees` is `onlyOwner` and only withdraws `totalFees`, not user contributions.

  • The core protocol function — pooling matched users' ETH into a shared multisig for their date — fails silently on every match. The `Matched` event fires and a multisig is deployed, but it holds no funds, breaking the entire product's value proposition.

Proof of Concept

  1. Alice mints a SoulboundProfileNFT.

  2. Bob mints a SoulboundProfileNFT.

  3. Alice calls likeUser(bob) with 1 ETH. Contract balance becomes 1 ETH. userBalances[alice] remains 0.

  4. Bob calls likeUser(alice) with 1 ETH. Mutual match is triggered. Contract balance becomes 2 ETH.

  5. matchRewards(alice, bob) executes internally:

    • Reads userBalances[alice] → 0

    • Reads userBalances[bob] → 0

    • Computes totalRewards = 0, matchingFees = 0, rewards = 0

  6. A new MultiSigWallet is deployed for Alice and Bob and funded with 0 ETH.

  7. Final state: 2 ETH is trapped in LikeRegistry. Neither Alice nor Bob has any function they can call to recover it.

// Add to a Foundry test file, e.g., test/LikeRegistryTest.t.sol
function test_ETHTrappedOnMatch() public {
address alice = makeAddr("alice");
address bob = makeAddr("bob");
vm.deal(alice, 10 ether);
vm.deal(bob, 10 ether);
// Both users mint profiles
vm.prank(alice);
profileNFT.mintProfile("Alice", 25, "ipfs://alice");
vm.prank(bob);
profileNFT.mintProfile("Bob", 27, "ipfs://bob");
// Alice likes Bob with 1 ETH
vm.prank(alice);
likeRegistry.likeUser{value: 1 ether}(bob);
// Bob likes Alice back with 1 ETH -> triggers match
vm.prank(bob);
likeRegistry.likeUser{value: 1 ether}(alice);
// Assert: 2 ETH is trapped in LikeRegistry
assertEq(address(likeRegistry).balance, 2 ether);
// Assert: userBalances were never updated
assertEq(likeRegistry.userBalances(alice), 0);
assertEq(likeRegistry.userBalances(bob), 0);
// Assert: the deployed multisig received 0 ETH
address[] memory aliceMatches = likeRegistry.getMatches();
// (The multisig address would need to be captured from event logs in a real test)
}

Recommended Mitigation

The root cause is that likeUser never persists the caller's contribution. Because userBalances is the sole source of truth matchRewards reads when computing payouts, and nothing writes to it in likeUser, every match reads zero and the multisig receives no funds.

The fix is a single-line addition inside likeUser, placed after the require checks and before the state changes for likes. This preserves the checks-effects-interactions ordering: input validation completes, the caller's contribution is recorded, then the like relationship is written.

The += operator is important — it accumulates across multiple likes from the same user, so a user who likes 3 profiles at 1 ETH each will have userBalances[user] == 3 ether at match time. After the fix, the multisig receives (userBalances[from] + userBalances[to]) * 90 / 100 on every match, and the fee accounting continues to work as intended.

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;
// ... rest unchanged
}
Updates

Lead Judging Commences

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