A user pays at least 1 ETH to like another profile. The README says: "If the like is mutual, all their previous like payments (minus a 10% fee) are pooled into a shared multisig wallet, which both users can access for their first date." matchRewards is written to do this. It reads both users' userBalances, adds 10% to totalFees, deploys a MultiSigWallet for the pair and sends it the other 90%. The owner collects the fees with withdrawFees.
likeUser checks msg.value (L32) but never records it. userBalances (L22) is never incremented anywhere in the codebase; its only uses are the reads and zeroing in matchRewards (L51-L54). So totalRewards, matchingFees and rewards are always 0. totalFees never grows, every match multisig is sent 0 wei (L65), and withdrawFees always reverts at L74. LikeRegistry has only these two ETH outflows, and there is no refund, sweep or rescue function. Every wei paid to likeUser, including any amount above 1 ETH, therefore stays in the contract permanently.
Likelihood:
It happens on every call to likeUser, in normal use, with no attacker and no special state: each payment is left uncredited the moment it is made.
It happens on every mutual like: each match deploys a MultiSigWallet and sends it rewards, which is always 0.
Impact:
All ETH paid to likeUser is permanently locked in LikeRegistry. This covers matched and unmatched likes and any overpayment above 1 ETH. In the PoC, four likes (one of them a 3 ETH overpayment) and two matches leave 6 ETH in the registry that no function can move.
Matched couples receive nothing and the owner never earns the 10% fee. Both match multisigs hold 0 wei, and the couple's 1 ETH withdrawal from one of them reverts Transaction failed. totalFees stays 0, so withdrawFees reverts No fees to withdraw. The core value flow the README promises never works.
The PoC deploys SoulboundProfileNFT and LikeRegistry directly, with no ffi, scripts or other test files. It also includes FixedLikeRegistry as a baseline. This is a copy of LikeRegistry with only the missing line userBalances[msg.sender] += msg.value; added. On it, a mutual 1 ETH + 1 ETH like sends 1.8 ETH to the multisig, and the owner withdraws 0.2 ETH of fees, leaving the registry empty. The baseline also confirms that the private totalFees is at storage slot 2, which the bug test reads with vm.load.
On the real LikeRegistry, the same flow gives these results:
the registry holds 2 ETH;
the new multisig holds 0 wei;
totalFees is 0;
withdrawFees reverts;
the couple's approved 1 ETH multisig withdrawal reverts Transaction failed.
The test then covers overpayment and a second match:
Carol's 3 ETH unmatched like is not credited either.
A second match deploys a second empty multisig, leaving 6 ETH stranded.
Run from the repository root, with the file saved as test/PoC_DD-01.t.sol:
Result: Suite result: ok. 2 passed; 0 failed; 0 skipped. The logs show BUGGY registry stranded balance (wei): 6000000000000000000, both multisig balances at 0, and FIXED multisig balance (wei): 1800000000000000000.
Credit the payment when the like is recorded. This one line is the whole difference between the two registries in the PoC.
## 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.