DatingDapp

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

likeUser function fails to record sent ETH for each like, causing matchRewards function to send zero funds to the Multisig Wallet

[H-1] likeUser function fails to record sent ETH for each like, causing matchRewards function to send zero funds to the Multisig Wallet

Note:

The PoC and test for H-1 assume that the L-1 mitigation (MatchInfo struct, updated matches mapping, and updated getMatches return type) has already been applied.
Please review and apply L-1 first to avoid compilation errors and to correctly reproduce this issue.

Description:

The likeUser function requires users to send at least 1 ETH when liking another user, but it never records the sent ETH into the userBalances mapping. As a result, when a mutual like occurs and matchRewards is executed, both users’ balances are read as zero.

Because matchRewards relies entirely on userBalances[from] and userBalances[to] to calculate the reward amount, the deployed multisig wallet receives 0 ETH even though both users paid ETH during the like process. This breaks the core reward mechanism of the protocol and leads to permanent loss of user funds locked in the contract.

Risk:

Impact: High

  • The multisig wallet to receive 0 ETH even though both users paid at least 1 ETH.

  • User funds to become stuck permanently inside the LikeRegistry contract.

  • The core business logic (rewarding matched users) to completely fail.

Likelihood: High

This bug occurs on every successful match, not just in edge cases.

  • Any pair of users who mutually like each other will trigger matchRewards.

  • Since userBalances is never updated, the failure happens deterministically.

Proof of Concept:

  • Copy the code below to testSoulboundProfileNFT.t.sol.

  • Run command forge test --mt testLikeAndMatch -vvvv.

function testLikeAndMatch() public{
//Deal some ether to users
vm.deal(user, 1 ether);
vm.deal(user2, 1 ether);
// Mint profiles for both users
vm.prank(user);
soulboundNFT.mintProfile("Alice", 25, "ipfs://profileImage");
vm.prank(user2);
soulboundNFT.mintProfile("Bob", 30, "ipfs://profileImageBob");
// User1 likes User2
vm.prank(user);
likeRegistry.likeUser{value: 1 ether}(user2);
// User2 likes User1
vm.prank(user2);
likeRegistry.likeUser{value: 1 ether}(user);
// Check matches
vm.prank(user);
LikeRegistry.MatchInfo[] memory user1Matches = likeRegistry.getMatches();
vm.prank(user2);
LikeRegistry.MatchInfo[] memory user2Matches = likeRegistry.getMatches();
assertEq(user1Matches[0].user, user2, "User1 should have User2 as match");
assertEq(user2Matches[0].user, user, "User2 should have User1 as match");
// Check multisig addresses
address multisigAddressUser1 = user1Matches[0].multisig;
address multisigAddressUser2 = user2Matches[0].multisig;
assertEq(multisigAddressUser1, multisigAddressUser2, "Multisig addresses should match");
uint256 multsigbalance = address(multisigAddressUser1).balance;
uint256 likeRegistryBalance = address(likeRegistry).balance;
//multisig receive no ether because in LikeRegistry, userBalances are never updated when users like each other
assertEq(multsigbalance,0);
//user1 and user2 match rewards are stuck in LikeRegistry contract balance forever
assertEq(likeRegistryBalance, 2 ether);
}

Recommended Mitigation:

  • Put this following mitigation in conntract LikeRegistry:

    • Introduce an enum LikeStatus { None, Liked, Matched } and repurpose the existing Like struct to track both the cumulative ETH sent and the current like status, since the original Like struct is unused.

    • Replace the likes mapping from mapping(address => mapping(address => bool)) to mapping(address => mapping(address => Like)) in order to persist both the like status and the total amount sent per user pair.

    • Introduce a constant MIN_LIKE_AMOUNT to replace the hardcoded minimum ETH requirement and reduce gas usage.

    • Update the likeUser function to:

      • Enforce the minimum amount using the constant.

      • Prevent duplicate likes or re-matches by checking the enum status.

      • Accumulate the sent ETH in the Like struct.

    • Update the matchRewards function to:

      • Read balances directly from the Like struct instead of userBalances.

      • Validate that both users have contributed the minimum amount.

      • Reset stored balances after rewards are processed.

      Persist and emit the deployed multisig wallet address together with match metadata.

+ uint256 public constant MIN_LIKE_AMOUNT = 1 ether; //save gas
+ enum LikeStatus { None, Liked, Matched }
struct Like { //audit-info: this struct is not being used
- address liker;
- address liked;
- uint256 timestamp;
+ uint256 totalSent;
+ LikeStatus status;
}
- mapping(address => uint256) public 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.value >= MIN_LIKE_AMOUNT, "Must send at least 1 ETH"); //audit-change
+ require(likes[msg.sender][liked].status == LikeStatus.None, "Already liked or matched"); //audit-change to check for enum status
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;
+ Like storage l = likes[msg.sender][liked];
+ l.status = LikeStatus.Liked;
+ l.totalSent = msg.value;
emit Liked(msg.sender, liked);
// Check if mutual like
- if (likes[liked][msg.sender]) {
+ if (likes[liked][msg.sender].status == LikeStatus.Liked) {
+ likes[msg.sender][liked].status = LikeStatus.Matched;
+ likes[liked][msg.sender].status = LikeStatus.Matched;
matchRewards(liked, msg.sender);
}
}
function matchRewards(address from, address to) internal {
- uint256 matchUserOne = userBalances[from];
- uint256 matchUserTwo = userBalances[to];
- userBalances[from] = 0;
- userBalances[to] = 0;
+ Like storage like1 = likes[from][to];
+ Like storage like2 = likes[to][from];
+ uint256 matchUserOne = like1.totalSent;
+ uint256 matchUserTwo = like2.totalSent;
+ require(matchUserOne >= MIN_LIKE_AMOUNT && matchUserTwo >= MIN_LIKE_AMOUNT, "Both user balance need to has at least 1 ETH");
+ like1.totalSent = 0;
+ like2.totalSent = 0;
uint256 totalRewards = matchUserOne + matchUserTwo;
uint256 matchingFees = (totalRewards * FIXEDFEE) / 100;
uint256 rewards = totalRewards - matchingFees;
totalFees += matchingFees;
// Deploy a MultiSig contract for the matched users
MultiSigWallet multiSigWallet = new MultiSigWallet(from, to);
matches[from].push(
MatchInfo({ user: to, multisig: address(multiSigWallet) })
);
matches[to].push(
MatchInfo({ user: from, multisig: address(multiSigWallet) })
);
// Send ETH to the deployed multisig wallet
(bool success,) = payable(address(multiSigWallet)).call{value: rewards}("");
require(success, "Transfer failed");
emit Matched(from, to, address(multiSigWallet));
}
Updates

Lead Judging Commences

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