likeUser() fails to credit userBalances, causing matched users to receive 0 ETHSeverity: High
When a user calls likeUser() and sends ETH, the contract does not increase userBalances[msg.sender]. As a result, when a mutual like occurs and matchRewards() is executed, both users’ recorded balances can remain 0, so the newly created multisig wallet receives 0 ETH instead of the intended pooled like payments minus fees.
This is a fund-accounting flaw. Solidity’s documentation emphasizes that contract state is the source of truth for assets and that balance-related invariants must be maintained correctly; otherwise, the contract can hold Ether that is not reflected in internal accounting. ([Solidity][1])
In LikeRegistry.likeUser():
The function accepts ETH but never does:
Later, matchRewards() calculates rewards only from userBalances:
So the contract receives Ether, but its accounting layer does not record that Ether for the users who paid it. This breaks the intended reward flow and can leave Ether stranded in the registry contract. Solidity’s docs also note that low-level value transfers are sensitive and must be paired with correct state handling. ([Solidity][1])
Two users can each pay at least 1 ETH, successfully match, and still receive 0 ETH in their shared multisig wallet. The intended “first date pool” is never funded even though the registry contract has already collected the funds.
This creates two concrete risks:
Loss of intended user funds utility: matched users do not receive the ETH they paid toward the match.
Accounting mismatch / stuck Ether: address(this).balance can exceed the sum of tracked liabilities because deposits are accepted but not assigned to userBalances.
Given that handling and tracking Ether correctly is core to this protocol’s functionality, this issue is high severity. ([Solidity][1])
The contract updates the social state (likes, matches) but does not update the financial state (userBalances) when receiving ETH in likeUser().
LikeRegistry is deployed with a valid SoulboundProfileNFT address.
Both users own profile NFTs.
likeUser() is callable and payable.
User A sends at least 1 ETH to like User B.
User B later sends at least 1 ETH to like User A.
User A calls likeUser(B) with 1 ETH.
Contract records the like, but does not increment userBalances[A].
User B calls likeUser(A) with 1 ETH.
Contract records the mutual match and calls matchRewards(B, A).
matchRewards() reads userBalances[B] == 0 and userBalances[A] == 0.
totalRewards becomes 0.
A MultiSigWallet is deployed and funded with 0 ETH.
The LikeRegistry contract still holds the ETH paid by the users, but that ETH is not represented in userBalances.
The following Foundry test demonstrates the issue.
This test shows that:
both users successfully pay 1 ETH,
a match occurs,
both userBalances remain 0,
the registry contract still holds the 2 ETH,
therefore the match reward calculation uses zero-value balances and cannot fund the multisig as intended.
Credit the sender’s balance inside likeUser() before match settlement:
If the protocol is intended to charge exactly 1 ETH per like, change the validation to:
That reduces ambiguity and simplifies accounting.
It is also advisable to assert an accounting invariant in tests, such as ensuring that contract-held Ether equals the sum of tracked liabilities plus fees, since Solidity recommends careful reasoning about state and Ether flows. ([Solidity][1])
Because the multisig wallet address is not stored or emitted, it is hard for users and tests to verify where funds were sent. Emitting the wallet address in an event would make settlement observable and much easier to audit.
A good event would be:
## 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); [...] } ```
The contest is live. Earn rewards by submitting a finding.
Submissions are being reviewed by our AI judge. Results will be available in a few minutes.
View all submissionsThe contest is complete and the rewards are being distributed.