DatingDapp

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

likeUser() requires >=1 ETH but never credits userBalances, so deposited ETH is orphaned and matches reward nothing

Root + Impact

Description

likeUser() is payable and requires the caller to send at least 1 ETH, but the function body never records that ETH against the user. There is no userBalances[msg.sender] += msg.value anywhere:

function likeUser(address liked) external payable {
require(msg.value >= 1 ether, "Must send at least 1 ETH");
...
likes[msg.sender][liked] = true;
emit Liked(msg.sender, liked);
@> // msg.value is NEVER added to userBalances[msg.sender]
if (likes[liked][msg.sender]) {
...
matchRewards(liked, msg.sender);
}
}

When a mutual like triggers matchRewards, it reads userBalances[from] and userBalances[to] - both still 0 - so totalRewards is 0, the freshly deployed multisig receives 0, and totalFees never grows. The entire premise ("all previous like payments are pooled into the match's multisig") fails. Worse, every 1+ ETH a user sends to likeUser simply accumulates in the contract with no accounting and no withdrawal path: matchRewards distributes the (zero) balances and withdrawFees can only pay out totalFees (also 0). All ETH deposited via likes is permanently locked.

Risk

Likelihood: High - occurs on every single likeUser call; it is the only way to use the protocol.

Impact: High - 100% of user deposits are orphaned with no recovery path, and matched couples receive nothing. Total loss of all funds flowing through the protocol.

Proof of Concept

Two users mint profiles and like each other with 1 ETH each. Their balances are never credited, the match pays out 0, and 2 ETH is stuck in the registry. Runnable, self-contained Foundry test:

import {LikeRegistry} from "../src/LikeRegistry.sol";
import {SoulboundProfileNFT} from "../src/SoulboundProfileNFT.sol";
function test_PoC_likeUserNeverCreditsBalances() public {
SoulboundProfileNFT nft = new SoulboundProfileNFT();
LikeRegistry reg = new LikeRegistry(address(nft));
address alice = makeAddr("alice");
address bob = makeAddr("bob");
vm.deal(alice, 1 ether);
vm.deal(bob, 1 ether);
vm.prank(alice); nft.mintProfile("alice", 30, "img");
vm.prank(bob); nft.mintProfile("bob", 30, "img");
vm.prank(alice); reg.likeUser{value: 1 ether}(bob);
assertEq(reg.userBalances(alice), 0); // deposit not credited
vm.prank(bob); reg.likeUser{value: 1 ether}(alice); // mutual -> match -> matchRewards
// balances still 0, so the match rewarded nothing; all 2 ETH is stuck in the registry
assertEq(reg.userBalances(alice), 0);
assertEq(reg.userBalances(bob), 0);
assertEq(address(reg).balance, 2 ether); // orphaned, no withdrawal path exists
}

Run forge test --mt test_PoC_likeUserNeverCreditsBalances -vv; it passes - 2 ETH is trapped and the match paid out nothing.

Recommended Mitigation

Credit each like payment to the sender's balance so matchRewards has funds to pool. The corrected function:

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");
userBalances[msg.sender] += msg.value; // <-- credit the deposit
likes[msg.sender][liked] = true;
emit Liked(msg.sender, liked);
if (likes[liked][msg.sender]) {
matches[msg.sender].push(liked);
matches[liked].push(msg.sender);
emit Matched(msg.sender, liked);
matchRewards(liked, msg.sender);
}
}

Why this fixes it: with userBalances[msg.sender] += msg.value, each deposit is tracked, so matchRewards pools the correct totals into the multisig and totalFees accrues the 10% cut. Additionally, add a user-facing withdraw() so deposits from likes that never become a mutual match can be reclaimed - otherwise unmatched likers' ETH would still be stuck even with correct accounting.

Updates

Lead Judging Commences

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