LikeRegistry.likeUser is payable and requires msg.value >= 1 ether, but it never credits userBalances[msg.sender] (or any other ledger). Native ETH still lands in the contract via the payable call. The only outbound paths (matchRewards and withdrawFees) read userBalances / totalFees, both of which stay at zero after a one-way like.
The product docs state that like payments are held until a mutual match, then pooled (minus a 10% fee) into a shared multisig. Because the credit is missing, that invariant is broken on every like: the ETH is untracked surplus with no spender.
Contract: LikeRegistry
File: src/LikeRegistry.sol
Function: likeUser(address liked) (line 31)
Related state: userBalances (line 22), totalFees (line 18), matchRewards (line 50), withdrawFees (line 73)
Intended reserve model (from README + matchRewards):
A like deposits ≥1 ETH and should increase userBalances[liker].
On mutual like, matchRewards snapshots both balances, zeros them, takes FIXEDFEE (10%) into totalFees, and sends the rest to a new MultiSigWallet.
The owner later drains totalFees via withdrawFees.
What the code actually does:
likeUser never reads or writes userBalances or totalFees. The only write to userBalances is the zeroing in matchRewards:
withdrawFees only sends totalFees, which is incremented solely from that snapshot:
After a one-way like:
address(this).balance increases by msg.value
userBalances[*] == 0
totalFees == 0 → withdrawFees reverts No fees to withdraw
matchRewards is not entered
If a later mutual like does enter matchRewards, both snapshots are still 0, so rewards == 0 and matchingFees == 0. The previously deposited ETH remains in the registry forever. There is no rescue / sweep / user withdraw.
Solvency invariant that fails:
After one 1 ETH like: 1e18 != 0.
No privilege, flash loan, or collusion is required. An honest one-way like is enough.
Deploy SoulboundProfileNFT and LikeRegistry(profileNFT).
Alice calls mintProfile("Alice", 25, "ipfs://alice").
Bob calls mintProfile("Bob", 26, "ipfs://bob").
Alice calls likeUser(Bob) with value: 1 ether.
Observe:
Liked is emitted; matchRewards is not entered
address(LikeRegistry).balance == 1 ether
userBalances[Alice] == 0, userBalances[Bob] == 0
withdrawFees() reverts No fees to withdraw
Optional: Bob likes Alice back. A MultiSigWallet is deployed and sent 0 ETH. The 2 ETH (Alice’s + Bob’s) stays locked in LikeRegistry.
Run:
Expected: assertion fail 1e18 != 0. Trace shows Liked emitted, matchRewards not entered, withdrawFees revert, registry balance 1 ether.
Critical — permanent loss of funds (broken reserve accounting).
Every like principal (≥1 ETH per call) is untracked surplus with no outbound path.
Users cannot recover a one-way like.
On a later match, matchRewards sends 0 because both userBalances are 0, so the date-fund multisig is empty and the 10% protocol fee is never accrued.
The owner cannot withdrawFees the stranded ETH (totalFees == 0).
Any profiled user reproduces this with a single 1 ETH like. At intended mainnet usage this drains every like the protocol ever receives.
Credit the deposit before the match check, and keep the solvency invariant test:
Also consider:
Enforce msg.value == 1 ether (or document overpay as intentional stake).
Add assert(address(this).balance >= sum(userBalances) + totalFees) (or a Foundry invariant) so a missing credit cannot ship again.
Decide whether unmatched likes should be user-withdrawable after a timeout; today there is still no unmatched-withdraw even after the credit fix, which may be intended.
## 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.