DatingDapp

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

likeUser() never credits userBalances, so every ETH payment sent to like someone is permanently and unconditionally stuck in LikeRegistry

Summary

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.

Description

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);
if (likes[liked][msg.sender]) {
matches[msg.sender].push(liked);
matches[liked].push(msg.sender);
emit Matched(msg.sender, liked);
matchRewards(liked, msg.sender);
}
}
function matchRewards(address from, address to) internal {
uint256 matchUserOne = userBalances[from]; // @> always 0 -- never written anywhere
uint256 matchUserTwo = userBalances[to]; // @> always 0 -- never written anywhere
userBalances[from] = 0;
userBalances[to] = 0;
uint256 totalRewards = matchUserOne + matchUserTwo; // @> always 0
uint256 matchingFees = (totalRewards * FIXEDFEE) / 100; // @> always 0
uint256 rewards = totalRewards - matchingFees; // @> always 0
totalFees += matchingFees; // @> totalFees never increases above 0 either
MultiSigWallet multiSigWallet = new MultiSigWallet(from, to);
(bool success,) = payable(address(multiSigWallet)).call{value: rewards}(""); // @> sends 0 ETH
require(success, "Transfer failed");
}

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.

Risk

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.

Proof of Concept

function test_H1_likePaymentsPermanentlyStuck_mutualMatchGetsZeroReward() public {
vm.prank(alice);
registry.likeUser{value: 1 ether}(bob);
assertEq(registry.userBalances(alice), 0, "userBalances never incremented on like");
vm.prank(bob);
registry.likeUser{value: 1 ether}(alice); // triggers the mutual match
// Both payments (2 ETH total) are stuck in the registry -- the deployed
// MultiSig received 0 ETH, since matchRewards() paid out from
// userBalances, which was 0 for both matched users.
assertEq(address(registry).balance, 2 ether, "all deposited ETH is stuck in the registry");
// No recovery path exists, not even for the owner:
vm.prank(registry.owner());
vm.expectRevert("No fees to withdraw");
registry.withdrawFees();
}

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.

Recommended Mitigation

Credit the payment when it's received:

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;
likes[msg.sender][liked] = true;
emit Liked(msg.sender, liked);
...
Updates

Lead Judging Commences

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