DatingDapp

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

Missing accounting of `msg.value` in `likeUser` leads to permanent loss of user funds

Root + Impact

Description

  • Normally, when a user calls likeUser and attaches the required msg.value >= 1 ether, that deposit should be credited to the user's balance so it can later be paid out as a reward once a mutual match occurs.

  • Instead, likeUser accepts and requires the ETH but never writes it to userBalances[msg.sender]. Since the mapping defaults to 0, matchRewards computes rewards from empty balances, sends 0 ETH to the newly deployed MultiSig wallet, and leaves every user's deposited ETH permanently stuck in the LikeRegistry contract.

// In likeUser() — msg.value is required but never recorded
require(msg.value >= 1 ether, "Must send at least 1 ETH");
// @> userBalances[msg.sender] is never incremented here
// ...
function matchRewards(address from, address to) internal {
// @audit-issue Both balances evaluate to 0 because they were never updated in likeUser()
uint256 matchUserOne = userBalances[from];
uint256 matchUserTwo = userBalances[to];
uint256 totalRewards = matchUserOne + matchUserTwo; // equals 0
uint256 matchingFees = (totalRewards * FIXEDFEE) / 100; // equals 0
uint256 rewards = totalRewards - matchingFees; // equals 0
totalFees += matchingFees; // totalFees remains 0
MultiSigWallet multiSigWallet = new MultiSigWallet(from, to);
// Transfers 0 ETH to the MultiSig, leaving the actual deposited ETH trapped
(bool success,) = payable(address(multiSigWallet)).call{value: rewards}("");
require(success, "Transfer failed");
}

Risk

Likelihood:

  • Any user who calls likeUser with the minimum required msg.value triggers the bug — this is the only way to interact with the function, so it happens on every single call, not as an edge case.

  • A match forming between two users (a mutual like) is a core, expected protocol flow, guaranteeing matchRewards is invoked on zero-value balances every time.
    Impact:

  • 100% Fund Loss: All ETH deposited by users to like other profiles gets permanently trapped inside the LikeRegistry contract.

  • Broken Protocol Mechanics: Matched users receive 0 ETH in their newly deployed MultiSig wallet, defeating the core purpose of the protocol.

  • Loss of Revenue: The protocol owner cannot withdraw any collected fees because totalFees evaluates to zero.

Proof of Concept

The test has two users, alice and bob, each call likeUser on the other while attaching the required 1 ETH, which forms a mutual match and triggers matchRewards internally. The assertion confirms the contract's own balance holds the full 2 ETH deposited by both users — proving the ETH was accepted but never routed to the MultiSig wallet or credited anywhere recoverable. The final check shows the owner's attempt to withdraw protocol fees reverts with "No fees to withdraw", confirming totalFees was never incremented despite 2 ETH sitting unaccounted-for in the contract.

Steps to Reproduce:

  1. Save the following Foundry test in test/LikeRegistryExploit.t.sol.

  2. Run forge test --match-test test_Exploit_TrappedFunds.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../src/LikeRegistry.sol";
contract MockProfileNFT {
function profileToToken(address) external pure returns (uint256) {
return 1;
}
}
contract LikeRegistryExploitTest is Test {
LikeRegistry public registry;
MockProfileNFT public mockNFT;
address public alice = address(0xAAAA);
address public bob = address(0xBBBB);
function setUp() public {
mockNFT = new MockProfileNFT();
registry = new LikeRegistry(address(mockNFT));
vm.deal(alice, 5 ether);
vm.deal(bob, 5 ether);
}
function test_Exploit_TrappedFunds() public {
vm.prank(alice);
registry.likeUser{value: 1 ether}(bob);
vm.prank(bob);
registry.likeUser{value: 1 ether}(alice);
assertEq(address(registry).balance, 2 ether);
vm.prank(registry.owner());
vm.expectRevert("No fees to withdraw");
registry.withdrawFees();
}
}

Recommended Mitigation

Update userBalances[msg.sender] inside likeUser to credit the deposited msg.value before any matching logic runs. This ensures that by the time matchRewards reads userBalances[from] and userBalances[to], both values correctly reflect the ETH each user actually deposited, allowing rewards, fees, and the MultiSig transfer to be computed and paid out correctly instead of defaulting to zero.

--- a/src/LikeRegistry.sol
+++ b/src/LikeRegistry.sol
@@ -34,6 +34,8 @@
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);
// Check if mutual like
Updates

Lead Judging Commences

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