DatingDapp

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

User funds are permanently frozen in LikeRegistry due to missing userBalances state update

Root + Impact

Description

  • The likeUser function is designed to collect ETH from users to be used as a reward for a potential match.

  • However, the contract fails to record the incoming msg.value into the userBalances mapping.

  • When a mutual like occurs, matchRewards calculates the payout based on userBalances. Since these are never updated, the reward is calculated as 0, leaving the original ETH trapped in the LikeRegistry contract.

// Root cause in the codebase:
function likeUser(address liked) external payable {
// ... validation logic ...
// @> The value is sent to the contract, but userBalances is never incremented
likes[msg.sender][liked] = true;
// ...
}

Risk

Likelihood:

  • This issue occurs every time a match is made.

  • There is no manual withdraw function, so any ETH sent to the contract is immediately at risk.

Impact:

  • High: 100% loss of user funds for every matched pair.

  • High: The MultiSigWallet intended for the couple remains empty, breaking the core utility of the protocol.

Proof of Concept

Step-by-step reproduction:

  1. Setup a LikeRegistry and SoulboundProfileNFT.

  2. Two users (Alice and Bob) mint their profiles.

  3. Alice likes Bob with 1 ETH.

  4. Bob likes Alice with 1 ETH.

  5. The contract correctly identifies a match and deploys a MultiSigWallet.

  6. The Bug: Check the balance of the newly created MultiSigWallet. It will be 0 ETH instead of the expected 1.8 ETH (2 ETH minus 10% fee).

  7. The full 2 ETH remains stuck in the LikeRegistry contract with no way to retrieve it.

// copy this code to your test/testLikeRegistry.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../src/LikeRegistry.sol";
import "../src/SoulboundProfileNFT.sol";
contract LikeRegistryTest is Test {
LikeRegistry likeRegistry;
SoulboundProfileNFT soulboundNFT;
address user1 = address(0x123);
address user2 = address(0x456);
address owner = address(this); // Test contract acts as the owner
function setUp() public {
soulboundNFT = new SoulboundProfileNFT();
likeRegistry = new LikeRegistry(address(soulboundNFT));
// Mint profiles for both users
vm.prank(user1);
soulboundNFT.mintProfile("Alice", 25, "ipfs://profileImage1");
vm.prank(user2);
soulboundNFT.mintProfile("Bob", 30, "ipfs://profileImage2");
vm.deal(user1, 10 ether);
vm.deal(user2, 10 ether);
}
function testLikeUser() public {
vm.prank(user1);
likeRegistry.likeUser{value: 1 ether}(user2);
bool liked = likeRegistry.likes(user1, user2);
assertTrue(liked, "User1 should have liked User2");
}
function testMutualLikeAndMatch() public {
// Record logs to check emitted events
vm.recordLogs();
// User1 likes User2
vm.prank(user1);
likeRegistry.likeUser{value: 1 ether}(user2);
// User2 likes User1
vm.prank(user2);
likeRegistry.likeUser{value: 1 ether}(user1);
// Get recorded logs to verify events
Vm.Log[] memory entries = vm.getRecordedLogs();
// Check if they are matched
vm.prank(user1);
address[] memory matchesUser1 = likeRegistry.getMatches();
vm.prank(user2);
address[] memory matchesUser2 = likeRegistry.getMatches();
assertEq(matchesUser1.length, 1, "User1 should have one match");
assertEq(matchesUser2.length, 1, "User2 should have one match");
assertEq(matchesUser1[0], user2, "User1's match should be User2");
assertEq(matchesUser2[0], user1, "User2's match should be User1");
// Check balance of shared wallet (MultiSig) to ensure it holds the correct amoun
address sharedWallet = vm.computeCreateAddress(address(likeRegistry), 1);
console.log("MultiSig balance (should be 1.8 ETH):", sharedWallet.balance);
assertEq(sharedWallet.balance, 1.8 ether, "Shared wallet should have 1.8 ETH");
}
}

To run the test: forge test --match-path test/testLikeRegistry.t.sol -vvvv

Recommended Mitigation

Explanation: The vulnerability stems from the contract's failure to update internal accounting. By adding userBalances[msg.sender] += msg.value;, we ensure the contract tracks how much each user has deposited. This allows the matchRewards function to pull the correct values when calculating the total payout for the match.

function likeUser(address liked) external payable {
if (msg.value < MIN_FEE) revert LikeRegistry__InsufficientFee();
// ...
+ userBalances[msg.sender] += msg.value;
likes[msg.sender][liked] = true;
// ...
}
Updates

Lead Judging Commences

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