LikeRegistry::likeUser requires callers to send at least 1 ETH, but never records that payment into userBalances. matchRewards -- the function that is supposed to forward a matched pair's pooled payments (minus a 10% fee) into their shared MultiSigWallet -- reads its payout amount entirely from userBalances, which is always 0. As a result, every single ETH payment ever sent through likeUser is permanently trapped in the LikeRegistry contract, with no function anywhere in the codebase that can ever move it out again -- not to the matched users, not to the fee owner, not to anyone.
likeUser is payable and genuinely receives msg.value (>= 1 ETH) from the caller -- that ETH really does leave the caller's wallet and land in the LikeRegistry contract's balance (helped along by the contract's own receive() external payable {}). But nowhere in likeUser, nowhere in matchRewards, and nowhere else in the contract is userBalances[msg.sender] ever incremented by the received msg.value. userBalances is only ever read (in matchRewards, always as 0) and zeroed -- never written to a nonzero value.
Consequently:
On a mutual match, matchRewards deploys a real MultiSigWallet for the pair, but forwards it exactly 0 ETH (rewards is always 0), completely defeating the app's entire stated purpose ("all their previous like payments (minus a 10% fee) are pooled into a shared multisig wallet").
totalFees also never grows above 0, since it's derived from the same always-zero accounting -- so withdrawFees() permanently reverts with "No fees to withdraw" for the owner too.
There is no refund function, no sweep function, no rescue mechanism of any kind for the ETH sitting in LikeRegistry. Every ETH ever sent via likeUser is gone forever, for every single user, on every single normal use of the app -- no attacker or adversarial behavior is required at all.
Likelihood:
Triggers on 100% of normal, intended usage -- literally every call to likeUser (the app's core, advertised feature) loses the caller's ETH. No special conditions, timing, or malicious actors needed.
Impact:
Complete, unconditional, permanent loss of every user's funds sent through the core feature of the protocol, with zero recovery path for users, matched pairs, or even the contract owner.
Run with forge test --match-test test_H1_likePaymentsPermanentlyStuck_mutualMatchGetsZeroReward -vv. After a complete, normal match cycle between two users, the registry holds the full 2 ETH deposited, the matched pair's MultiSig wallet received nothing, and withdrawFees() reverts -- confirming the funds are permanently and unconditionally unrecoverable.
Credit the payment when it's received:
## 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.