DatingDapp

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

userBalance never got increment

Title

LikeRegistry::likeUser never updates userBalances, locking all user ETH and breaking reward distribution and fee collection

Root + Impact

Description

  • Users must send at least 1 ETH to likeUser, and these deposits are meant to be tracked in userBalances. On a mutual like, matchRewards reads both users' balances, keeps 10% (FIXEDFEE) as totalFees, and sends the remaining 90% to a newly deployed MultiSigWallet shared by the two users.

  • However, likeUser never adds msg.value to userBalances, so the mapping is always 0. matchRewards therefore computes totalRewards = 0, sends 0 ETH to the MultiSigWallet, and accrues 0 fees. The ETH sent by users remains in LikeRegistry, which has no function that returns it to them.

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 never added 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
...
}

Risk

Likelihood: High

  • This occurs on every call to likeUser by every user. No attacker, special condition, or privileged access is required.

Impact: High

  • All ETH sent through likeUser is permanently locked in LikeRegistry. No function sends it back to users, and withdrawFees can only send totalFees, which is always 0. This applies to every like, including likes that are never reciprocated.

  • On a mutual like, the MultiSigWallet is deployed with a 0 balance instead of 90% of the two users' combined deposits (1.8 ETH for two deposits of 1 ETH).

  • totalFees is always 0, so the protocol never collects fees and withdrawFees always reverts.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
​
import "forge-std/Test.sol";
import "../src/SoulboundProfileNFT.sol";
import "../src/LikeRegistry.sol";
​
contract LikeRegistryBugTest is Test {
SoulboundProfileNFT nft;
LikeRegistry registry;
​
address public alice = makeAddr("alice");
address public bob = makeAddr("bob");
address public owner = makeAddr("owner");
​
function setUp() external {
vm.prank(owner);
nft = new SoulboundProfileNFT();
registry = new LikeRegistry(address(nft));
​
vm.deal(alice, 1 ether);
vm.deal(bob, 1 ether);
}
​
function testUserBalancesNotUpdatedLocksEth() public {
vm.prank(alice);
nft.mintProfile("alice", 25, "");
vm.prank(bob);
nft.mintProfile("bob", 25, "");
​
// address of the MultiSigWallet that matchRewards will deploy (next CREATE from registry)
address expectedMultiSig = computeCreateAddress(address(registry), vm.getNonce(address(registry)));
​
vm.prank(alice);
registry.likeUser{value: 1 ether}(bob);
vm.prank(bob);
registry.likeUser{value: 1 ether}(alice); // mutual like triggers matchRewards
​
assertEq(registry.userBalances(alice), 0); // never incremented
assertEq(registry.userBalances(bob), 0);
assertEq(expectedMultiSig.balance, 0); // should be 1.8 ether
assertEq(address(registry).balance, 2 ether); // all user ETH is locked in the registry
​
// fees are never collected (totalFees is not public, so prove it via withdrawFees)
vm.expectRevert("No fees to withdraw");
registry.withdrawFees(); // this test contract is the owner, since it deployed the registry
}
}

Recommended Mitigation

Credit the deposit to the sender (msg.sender) before the mutual-like check, because matchRewards reads userBalances.

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!