DatingDapp

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

System Design Allowing Multiple Mutual Matches Leads to Vulnerabilities

Summary

  • The system shouldn't work in a way that after a mutual like, another mutual like can still happen, but if that's the intended design, then it's prone to the following vulnerability

Vulnerability Details

  • Scenario: Imagine User A has already liked 5 users, including User B and User C. Upon User B liking User A back, creating a mutual match, all of User A's previous like payments are pooled into a MultiSig account with User B's payment for their first date. Even after a user has a mutual like, he or she can still like other users, leading to:

    • Inequitable Distribution of Funds: User A's like payments have already been allocated to the match with User B, leaving User C without the benefit of User A's previous contributions when User C matches with User A.

    • Front-Running Exposure: This setup incentivizes users or malicious actors to front-run the blockchain to match with someone just after they've matched, hoping to catch the remaining, larger pool of funds before it's depleted. This can lead to:

      • Disadvantage for Late Matchers: Users who match later with someone who has already matched receive less or no financial benefit, leading to unfair treatment.

      • Front-Running Vulnerability: The system becomes susceptible to front runnig where users rush to match with someone they know has recently matched to gain access to more funds.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../src/LikeRegistry.sol";
import "../src/SoulboundProfileNFT.sol";
import "../src/MultiSig.sol";
contract LikeRegistryTest is Test {
LikeRegistry likeRegistry;
SoulboundProfileNFT soulboundNFT;
address user = makeAddr("user");
address user1 = makeAddr("user1");
address user2 = makeAddr("user2");
address user3 = makeAddr("user3");
address user4 = makeAddr("user4");
address user5 = makeAddr("user5");
address owner = address(this); // Test contract acts as the owner
function setUp() public {
soulboundNFT = new SoulboundProfileNFT();
likeRegistry = new LikeRegistry(address(soulboundNFT));
vm.deal(user, 10 ether);
vm.deal(user1, 2 ether);
vm.deal(user2, 2 ether);
vm.deal(user4, 5 ether);
// Mint profile NFTs for both users
vm.prank(user);
soulboundNFT.mintProfile("User1", 25, "ipfs://profile1");
vm.prank(user1);
soulboundNFT.mintProfile("User1", 25, "ipfs://profile1");
vm.prank(user2);
soulboundNFT.mintProfile("User2", 26, "ipfs://profile2");
vm.prank(user3);
soulboundNFT.mintProfile("User1", 25, "ipfs://profile1");
vm.prank(user4);
soulboundNFT.mintProfile("User1", 25, "ipfs://profile1");
vm.prank(user5);
soulboundNFT.mintProfile("User2", 26, "ipfs://profile2");
}
function testMutualLikeRewards() public {
// Log initial balances
console.log("Initial balance of user1:", address(user1).balance);
console.log("Initial balance of user2:", address(user2).balance);
console.log("Initial balance of LikeRegistry:", address(likeRegistry).balance);
// Simulate users liking each other
vm.startPrank(user);
likeRegistry.likeUser{value: 1 ether}(user1);
likeRegistry.likeUser{value: 1 ether}(user2);
likeRegistry.likeUser{value: 1 ether}(user3);
likeRegistry.likeUser{value: 1 ether}(user4);
likeRegistry.likeUser{value: 1 ether}(user5);
vm.stopPrank();
vm.startPrank(user1);
likeRegistry.likeUser{value: 1 ether}(user);
likeRegistry.likeUser{value: 1 ether}(user2);
vm.stopPrank();
console.log("After mutual likes btw user and user1 - user balance:", address(user).balance);
console.log("After mutual likes btw user and user1 - user balance:", address(user1).balance);
console.log("before user2 likes user - user2 balance:", address(user2).balance);
console.log("After mutual likes btw user and user1 - LikeRegistry balance:", address(likeRegistry).balance);
vm.startPrank(user2);
likeRegistry.likeUser{value: 1 ether}(user);
vm.stopPrank();
console.log("After mutual like - user1 balance:", address(user1).balance);
console.log("After mutual like - user2 balance:", address(user2).balance);
console.log("After mutual like - LikeRegistry balance:", address(likeRegistry).balance);
//I had to add console.log multisig in the like contract to get the addresses cause it's returning something different in the test implemetation
address multiSigAddress1 = address(0xffD4505B3452Dc22f8473616d50503bA9E1710Ac);
address multiSigAddress2 = address(0x8d2C17FAd02B7bb64139109c6533b7C2b9CADb81);
console.log("MultiSig1 wallet balance:", address(multiSigAddress1).balance);
console.log("MultiSig2 wallet balance:", address(multiSigAddress2).balance);
console.log("LikeRegistry balance:", address(likeRegistry).balance);
}
}
the result
[⠊] Compiling...
No files changed, compilation skipped
Ran 1 test for test/testLiked.t.sol:LikeRegistryTest
[PASS] testMutualLikeRewards() (gas: 2403249)
Logs:
Initial balance of user1: 2000000000000000000
Initial balance of user2: 2000000000000000000
Initial balance of LikeRegistry: 0
Attempting transfer to MultiSig wallet: 0xffD4505B3452Dc22f8473616d50503bA9E1710Ac with amount: 5400000000000000000
rewards 5400000000000000000
Transfer to MultiSig successful, new balance: 5400000000000000000
After mutual likes btw user and user1 - user balance: 5000000000000000000
After mutual likes btw user and user1 - user balance: 0
before user2 likes user - user2 balance: 2000000000000000000
After mutual likes btw user and user1 - LikeRegistry balance: 1600000000000000000
Attempting transfer to MultiSig wallet: 0x8d2C17FAd02B7bb64139109c6533b7C2b9CADb81 with amount: 900000000000000000
rewards 900000000000000000
Transfer to MultiSig successful, new balance: 900000000000000000
After mutual like - user1 balance: 0
After mutual like - user2 balance: 1000000000000000000
After mutual like - LikeRegistry balance: 1700000000000000000
MultiSig1 wallet balance: 5400000000000000000
MultiSig2 wallet balance: 900000000000000000
LikeRegistry balance: 1700000000000000000
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 4.03ms (1.34ms CPU time)
Ran 1 test suite in 11.84ms (4.03ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
making multisig2 which is the multisig of the later mutual likes to receives 0.9 ether compared to 5.4 ether the first mutual likes receives

Impact

  • Financial Inequity: Users who match later are financially disadvantaged, which could lead to dissatisfaction and a perception of the system as unfair.

  • Manipulation of Matches: Encourages a race to match, potentially skewing the platform's purpose from genuine dating to financial exploitation.

Tools Used

Manual Review and foundry

Recommendations

  • Reconsider the Design of Mutual Match Mechanics:

    • Single Mutual Match per User: If the intent is to prevent this scenario, redesign so that once a user has a mutual match, they cannot create another until the first match is resolved or expires.

    • Or, Equitable Reward Distribution:

      • If multiple matches are intended, ensure each match pools only the mutual like payments, not all previous likes:

      solidity

      // In LikeRegistry.sol
      mapping(address => mapping(address => uint256)) public likePayments; // Track each like's payment
      function likeUser(address liked) external payable {
      // ... existing checks
      likes[msg.sender][liked] = true;
      likePayments[msg.sender][liked] = msg.value; // Store payment for this specific like
      // ... rest of the function
      }
      function matchRewards(address from, address to) internal {
      uint256 rewardsFrom = likePayments[from][to];
      uint256 rewardsTo = likePayments[to][from];
      delete likePayments[from][to];
      delete likePayments[to][from];
      uint256 totalRewards = rewardsFrom + rewardsTo;
      uint256 matchingFees = (totalRewards * FIXEDFEE) / 100;
      uint256 rewards = totalRewards - matchingFees;
      // Deploy and fund the MultiSigWallet with the calculated rewards
      MultiSigWallet multiSigWallet = new MultiSigWallet(from, to);
      (bool success,) = payable(address(multiSigWallet)).call{value: rewards}("");
      require(success, "Transfer to MultiSig failed");
      }

Additional Considerations:

  • Front-Running Prevention: By ensuring each match only involves the mutual like payments, the incentive for front-running is neutralized.

  • User Expectations: Clearly communicate the system's mechanics to users to set the right expectations about matching and rewards.

  • System Fairness: This redesign aims to maintain fairness across all users, potentially increasing user trust and reducing manipulative behaviors.

This approach would address the vulnerability by either limiting the number of mutual matches per user or by ensuring each match is treated equitably in terms of pooled funds.

Updates

Appeal created

n0kto Lead Judge 4 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.