DatingDapp

First Flight #33
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Severity: medium
Valid

LikeRegistry::likeUser logic can lead to a user bypassing contributing money to the dating pool if they match with someone.

Vulnerability Details

The LikeRegistry::likeUser function allows a user to like another user by providing their wallet address as a parameter. However, the function does not check whether the liked user has already matched with someone else. If they have, their funds are already pooled in a multisig wallet with another user. As a result, when a new user likes them, given, if they had liked the new user previously, a new multisig wallet is created with only the new liker’s funds, as the liked user’s balance is already zero.

Example Scenario

  • Bob, Alice, and Cathy are users of the platform.

  • Bob has already liked Alice and Cathy.

  • Cathy likes Bob back, creating a multisig wallet with their pooled funds.

  • Alice then likes Bob back, creating another multisig wallet. However, Bob’s balance is now zero, meaning only Alice’s funds are pooled.

Impact

  • The pool becomes single-sided, meaning only one person is funding the date.

  • An attacker can exploit this by creating two fake profiles and matching them (Bob and Cathy), then liking real users, and getting dates without paying any money.

Proof of Concept

To test this vulnerability, the following test case demonstrates how two multisig wallets are created, but one of them is single-sided due to missing funds from one of the users.

// Note: Before running this test, ensure the userBalances issue is fixed.
// Modified likeUser to return the address of the multisig wallets for verification.
function likeUser(address liked) external payable returns (MultiSigWallet multiSigWallet) {
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);
//userBalance is updated to keep track of user deposits
userBalances[msg.sender] += msg.value; <@
// Check if mutual like
if (likes[liked][msg.sender]) {
//push each other to matches
matches[msg.sender].push(liked);
matches[liked].push(msg.sender);
emit Matched(msg.sender, liked);
returns matchRewards(liked, msg.sender);
}
}
//@notice To pool the eth when a match occurs
function matchRewards(address from, address to) internal returns (MultiSigWallet multiSigWallet){
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;
// Deploy a MultiSig contract for the matched users
multiSigWallet = new MultiSigWallet(from, to);
// Send ETH to the deployed multisig wallet
(bool success, ) = payable(address(multiSigWallet)).call{value: rewards}("");
require(success, "Transfer failed");
}
function test_double_match() public {
vm.prank(user);
soulboundNFT.mintProfile("Alice", 25, "ipfs://profileImage");
vm.prank(user2);
soulboundNFT.mintProfile("Bob", 26, "ipfs://profileImage");
vm.prank(user3);
soulboundNFT.mintProfile("Cathy", 28, "ipfs://profileImage");
deal(user, 3 ether);// Alice
deal(user2, 3 ether);// Bob
deal(user3, 3 ether);// Cathy
// Bob likes both Alice and Cathy
vm.startPrank(user2);
likeRegistry.likeUser{value: 1 ether}(user);
likeRegistry.likeUser{value: 1 ether}(user3);
vm.stopPrank();
// Now Alice and Cathy both like Bob too
vm.prank(user); // Alice sends like first
MultiSigWallet multisig1 = likeRegistry.likeUser{value: 1 ether}(user2);
vm.prank(user3); // Then Cathy does it
MultiSigWallet multisig2 = likeRegistry.likeUser{value: 1 ether}(user2);
// This results in two multisig creations, but the Alice-Bob wallet will have 2+1 ether
// while Cathy-Bob will only have all of Cathy's money in it, so the Cathy-Bob wallet will have 1 ether.
vm.assertEq(address(multisig1).balance, 2.7 ether); // 10% fees
vm.assertEq(address(multisig2).balance, 0.9 ether);
}

Execution Trace

│ ├─ [627] SoulboundProfileNFT::profileToToken(0x0000000000000000000000000000000000000456) [staticcall]
│ │ └─ ← [Return] 2
│ ├─ emit Liked(liker: user3: [0xc0A55e2205B289a967823662B841Bd67Aa362Aec], liked: 0x0000000000000000000000000000000000000456)
│ ├─ emit Matched(user1: user3: [0xc0A55e2205B289a967823662B841Bd67Aa362Aec], user2: 0x0000000000000000000000000000000000000456)
│ ├─ [483834] → new MultiSigWallet@0xCB6f5076b5bbae81D7643BfBf57897E8E3FB1db9
│ │ └─ ← [Return] 2193 bytes of code
│ ├─ [55] MultiSigWallet::receive{value: 900000000000000000}()
│ │ └─ ← [Stop]
│ └─ ← [Return] MultiSigWallet: [0xCB6f5076b5bbae81D7643BfBf57897E8E3FB1db9]
│ ├─ [0] VM::assertEq(2700000000000000000 [2.7e18], 2700000000000000000 [2.7e18]) [staticcall]
│ │ └─ ← [Return]
│ ├─ [0] VM::assertEq(900000000000000000 [9e17], 900000000000000000 [9e17]) [staticcall]
│ │ └─ ← [Return]
│ └─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.26ms (404.05µs CPU time)

Recommendations

1. Ensure the Liked User Has Funds

Modify likeUser to check if the liked user has sufficient funds before proceeding:

function likeUser(address liked) external payable {
require(userBalances[liked] > 0, "Liked user currently has no money in the contract");
// existing logic
}

However, this could lead to scenarios where users are forced to like new profiles to deposit ETH before being able to match. To resolve this, modify the receive function to allow direct ETH deposits:

receive() external payable {
require(profileNFT.profileToToken(msg.sender) != 0, "Must have a profile NFT");
require(msg.value >= 1 ether, "Must send at least 1 ETH");
userBalances[msg.sender] += msg.value;
}

2. Restrict Users to One Match at a Time

Update the matches mapping to prevent multiple matches:

// Change mapping to allow only one active match
mapping(address => address) public match;

Modify likeUser to enforce this restriction:

function likeUser(address liked) external payable {
require(match[msg.sender] == address(0), "User currently has a match");
require(match[liked] == address(0), "Liked user currently has a match");
// existing logic
}

3. Add an unmatchUser Function

To allow users to reset their matches if a date does not go well, implement an unmatchUser function:

function unmatchUser() external {
require(match[msg.sender] != address(0), "No active match");
address previousMatch = match[msg.sender];
match[msg.sender] = address(0);
match[previousMatch] = address(0);
emit Unmatched(msg.sender, previousMatch);
}

This ensures users can move on if they wish to find a new match.

Updates

Appeal created

n0kto Lead Judge 6 months ago
Submission Judgement Published
Validated
Assigned finding tags:

finding_several_match_lead_to_multisig_with_no_funds

Likelihood: Medium, if anyone has 2 matches or more before reliking. Impact: Medium, the user won't contribute to the wallet.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.