DatingDapp

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

Matched Like Deposits Are Locked in LikeRegistry Instead of Funded to the Match Multisig.

Description

When two users mutually like each other, the protocol is expected to pool their paid like deposits, take a 10% protocol fee, and send the remaining ETH to a newly deployed MultiSigWallet for the matched pair.

The issue is that LikeRegistry.likeUser() accepts ETH but never credits the payment to userBalances[msg.sender]. As a result, when a mutual match occurs, matchRewards() reads zero balances for both users, deploys a MultiSigWallet, and sends 0 ETH to it. The actual ETH deposits remain stuck in LikeRegistry and cannot be withdrawn as fees because totalFees is also calculated from the zero internal balances.

// LikeRegistry.sol
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 accepted but never credited to userBalances[msg.sender]
likes[msg.sender][liked] = true;
if (likes[liked][msg.sender]) {
matches[msg.sender].push(liked);
matches[liked].push(msg.sender);
matchRewards(liked, msg.sender);
}
}
function matchRewards(address from, address to) internal {
// @> These values remain zero because likeUser() never credits deposits
uint256 matchUserOne = userBalances[from];
uint256 matchUserTwo = userBalances[to];
userBalances[from] = 0;
userBalances[to] = 0;
uint256 totalRewards = matchUserOne + matchUserTwo;
uint256 matchingFees = (totalRewards * FIXEDFEE) / 100;
uint256 rewards = totalRewards - matchingFees;
totalFees += matchingFees;
MultiSigWallet multiSigWallet = new MultiSigWallet(from, to);
// @> Sends 0 ETH to the match wallet
(bool success,) = payable(address(multiSigWallet)).call{value: rewards}("");
require(success, "Transfer failed");
}

Risk

Likelihood: High

  • This occurs for every successful mutual match because likeUser() never records the ETH sent by either user.

  • No special attacker behavior is required. Normal protocol usage triggers the issue when two valid profile owners like each other.

Impact: High

  • Matched users lose access to the ETH that should have funded their shared multisig wallet.

  • The deployed MultiSigWallet receives 0 ETH, while the paid deposits remain stuck in LikeRegistry.

  • The owner cannot recover the stranded ETH through withdrawFees() because totalFees is never increased from the uncredited deposits.

Proof of Concept

The following test shows Alice and Bob each paying 1 ETH to like each other. A mutual match is recorded, but the newly deployed multisig receives 0 ETH. The 2 ETH paid by users remains stuck in LikeRegistry, and withdrawFees() cannot recover it.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../src/SoulboundProfileNFT.sol";
import "../src/LikeRegistry.sol";
contract MatchedLikeDepositsLockedTest is Test {
SoulboundProfileNFT nft;
LikeRegistry registry;
address alice = address(0xA11CE);
address bob = address(0xB0B);
function setUp() public {
nft = new SoulboundProfileNFT();
registry = new LikeRegistry(address(nft));
vm.prank(alice);
nft.mintProfile("Alice", 25, "ipfs://alice");
vm.prank(bob);
nft.mintProfile("Bob", 25, "ipfs://bob");
vm.deal(alice, 2 ether);
vm.deal(bob, 2 ether);
}
function testMatchedLikeDepositsAreLockedInRegistry() public {
vm.prank(alice);
registry.likeUser{value: 1 ether}(bob);
uint64 registryNonceBeforeMatch = vm.getNonce(address(registry));
address deployedMultisig = vm.computeCreateAddress(
address(registry),
registryNonceBeforeMatch
);
vm.prank(bob);
registry.likeUser{value: 1 ether}(alice);
vm.prank(alice);
address[] memory aliceMatches = registry.getMatches();
vm.prank(bob);
address[] memory bobMatches = registry.getMatches();
assertEq(aliceMatches.length, 1);
assertEq(aliceMatches[0], bob);
assertEq(bobMatches.length, 1);
assertEq(bobMatches[0], alice);
// The match wallet was deployed, but it received no ETH.
assertEq(deployedMultisig.balance, 0);
// Both 1 ETH like payments are stuck in LikeRegistry.
assertEq(address(registry).balance, 2 ether);
// The stuck ETH is not accounted as protocol fees.
vm.expectRevert("No fees to withdraw");
registry.withdrawFees();
}
}

Recommended Mitigation

Credit each incoming like payment to the sender’s internal balance before checking for a mutual match. Then matchRewards() can correctly calculate the total reward, fees, and multisig funding amount.

diff --git a/contracts/LikeRegistry.sol b/contracts/LikeRegistry.sol
index abc1234..def5678 100644
--- a/contracts/LikeRegistry.sol
+++ b/contracts/LikeRegistry.sol
@@
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);
if (likes[liked][msg.sender]) {
matches[msg.sender].push(liked);

With this change, when Alice and Bob mutually match after each paying 1 ETH, matchRewards() sees 2 ETH in total user balances, records the 10% fee, and transfers the remaining 1.8 ETH to the deployed MultiSigWallet.

Optionally, the protocol should also decide how to handle overpayment above 1 ETH: either reject msg.value > 1 ether, refund the excess, or intentionally include the full amount in the user’s match balance.

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!