DatingDapp

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

`userBalances` never written in `likeUser()` -- all deposited ETH permanently locked

Description

Root + Impact

Description

  • The LikeRegistry protocol intends for users to deposit ETH when liking other users, track those deposits in userBalances, and upon mutual match distribute the pooled ETH (minus a 10% fee) to a newly deployed MultiSigWallet.

  • The likeUser() function accepts ETH via msg.value (requiring >= 1 ETH) but never writes to the userBalances mapping. When matchRewards() is triggered on a mutual match, it reads userBalances[from] and userBalances[to] -- both of which are always 0. This means totalRewards = 0, rewards = 0, and the MultiSig receives nothing. Additionally, totalFees is never incremented, so withdrawFees() always reverts. All ETH deposited by users is permanently locked in the contract with no recovery mechanism.

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");
// @> msg.value is received but NEVER credited to userBalances[msg.sender]
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
uint256 matchUserTwo = userBalances[to]; // @> always 0
userBalances[from] = 0;
userBalances[to] = 0;
uint256 totalRewards = matchUserOne + matchUserTwo; // @> 0 + 0 = 0
uint256 matchingFees = (totalRewards * FIXEDFEE) / 100; // @> 0
uint256 rewards = totalRewards - matchingFees; // @> 0
totalFees += matchingFees; // @> totalFees remains 0
MultiSigWallet multiSigWallet = new MultiSigWallet(from, to);
(bool success,) = payable(address(multiSigWallet)).call{value: rewards}(""); // @> sends 0 ETH
require(success, "Transfer failed");
}

Risk

Likelihood:

  • This occurs on every single likeUser() call -- the bug is in the core user flow and affects 100% of interactions

  • Every mutual match triggers matchRewards() which reads the always-zero userBalances

Impact:

  • All ETH deposited by users via likeUser() is permanently locked in the LikeRegistry contract

  • The MultiSigWallet deployed on match receives 0 ETH instead of the pooled rewards

  • withdrawFees() always reverts since totalFees is never incremented, so even the protocol owner cannot extract any funds

Proof of Concept

function testUserBalancesNeverWritten() public {
// Setup: mint profiles for alice and bob
vm.prank(alice);
profileNFT.mintProfile("Alice", 25, "img");
vm.prank(bob);
profileNFT.mintProfile("Bob", 26, "img");
// Alice likes Bob with 1 ETH
vm.prank(alice);
likeRegistry.likeUser{value: 1 ether}(bob);
// Bob likes Alice with 1 ETH -- triggers mutual match
vm.prank(bob);
likeRegistry.likeUser{value: 1 ether}(alice);
// 2 ETH is now permanently locked in LikeRegistry
assertEq(address(likeRegistry).balance, 2 ether);
// Owner cannot withdraw fees
vm.prank(owner);
vm.expectRevert("No fees to withdraw");
likeRegistry.withdrawFees();
}

Recommended Mitigation

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 2 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!