Summary
In the `LikeRegistry::likeUser`, we do not check if the user has a reciprocated like already. Two users who have been previously liked by a particular user can like the user back at thesame exact time because we do not check if the user has been matched already. The implication of this is that 2 match transactions will run simultaneously and the blockchain will decide the one that goes first.
Vulnerability Details
Proof of Concept:
1. UserA likes UserB and UserC profile.
2. UserB and UserC see UserA profile and decide to reciprocate the like, but they do it at thesame timestamp.
3. Since the transactions happen at thesame `block.timestamp`, the blockchain will act in a deterministic way. The transaction with the bigger values, and the more gas will process first before the smaller transaction. If UserA totalBalance is 3 ETH, UserB totalBalance is 5ETH, and UserC totalBalance is 2ETH. The shared multisignature wallet between UserA and UserB will contain 7.2ETH, while the shared wallet between UserA and UserC will contain 1.8ETH. In this system UserA gains more because they now have 2 wallets, UserB also gains since its fund is pooled together with UserA in their account. However, UserC is on the losing side because it doesn't gain anything from UserA.
4. This process is not restricted to a user being matched with 2 users alone as even only user can match with 10 people at thesame time if this is exploited.
Proof of Code:
<details>
<summary>Code</summary>
Add the following code to the `testSoulboundProfileNFT.t.sol` file.
```javascript
function testOneUserGetsMatchedWith2PeopleAtThesameTime() public {
LikeRegistry userRegistry = new LikeRegistry(address(soulboundNFT));
vm.prank(user); // Simulates user calling the function
soulboundNFT.mintProfile("Alice", 25, "ipfs://profileImage");
vm.prank(user2); // Simulates user2 calling the function
soulboundNFT.mintProfile("Ochuko", 24, "ipfs://profileImage");
vm.prank(user3); // Simulates user2 calling the function
soulboundNFT.mintProfile("Kevwe", 26, "ipfs://profileImage");
vm.deal(user, 3 * TRANSFER_AMOUNT);
vm.deal(user2, 3 * TRANSFER_AMOUNT);
vm.deal(user3, 3 * TRANSFER_AMOUNT);
address[] memory users = new address[](10);
for (uint256 i = 0; i < 10; i++) {
users[i] = address(uint160(uint256(keccak256(abi.encodePacked(i))) + 0x600));
}
for (uint256 i = 0; i < users.length; i++) {
vm.prank(users[i]); // Simulates user calling the function
soulboundNFT.mintProfile("Alice", 25, "ipfs://profileImage");
}
// Create LikeRegistry instances with the correct profile NFT address
for (uint256 i = 0; i < users.length; i++) {
vm.deal(users[i], 4 * TRANSFER_AMOUNT);
vm.prank(users[i]);
userRegistry.likeUser{value: TRANSFER_AMOUNT}(user);
}
for (uint256 i = 0; i < users.length; i++) {
vm.prank(users[i]);
userRegistry.likeUser{value: TRANSFER_AMOUNT}(user2);
}
for (uint256 i = 0; i < users.length; i++) {
vm.prank(users[i]);
userRegistry.likeUser{value: TRANSFER_AMOUNT}(user3);
}
//User likes user2
vm.prank(user);
userRegistry.likeUser{value: TRANSFER_AMOUNT}(user2);
//User also likes user3
vm.prank(user);
userRegistry.likeUser{value: TRANSFER_AMOUNT}(user3);
vm.warp(7200);
//User2 likes user back
vm.prank(user2);
userRegistry.likeUser{value: TRANSFER_AMOUNT}(user);
vm.warp(7200);
//User3 likes user back
vm.prank(user3);
userRegistry.likeUser{value: TRANSFER_AMOUNT}(user);
vm.prank(user);
address[] memory userMatches = userRegistry.getMatches();
vm.prank(user2);
address[] memory user2Matches = userRegistry.getMatches();
vm.prank(user3);
address[] memory user3Matches = userRegistry.getMatches();
assertEq(user2Matches[0], user3Matches[0]);
assertEq(userMatches.length, 2);
assertEq(userMatches[0], user2);
assertEq(userMatches[1], user3);
}
```
</details>
Impact
This will affect the `LikeRegistry::matchRewards` function. The user that their transaction processes first on the blockchain gets the reward of his balances and the user matched with balances. The other user whose transaction is processed after get his balance plus 0 from the matched user because the matched user's balance is already set to 0. This causes a disadvantage to people, and it is no longer a fair system.
Recommendations
To fix this, we can add a mapping that tracks addresses that are matched and update these addresses in the `LikeRegistry::likeUser` function. The whole issue stems from here [LikeRegistry::likeUser](https://github.com/CodeHawks-Contests/2025-02-datingdapp/blob/878bd34ef6607afe01f280cd5aedf3184fc4ca7b/src/LikeRegistry.sol#L42).
##NOTE: we still need to update this function to let users match again if they are done with their dates.
```diff
// add this global variable to track matched users
+ mapping(address => bool) public matched;
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]) {
+ require(!matched[msg.sender] && !matched[liked], "Already matched user cannot match again");
matches[msg.sender].push(liked);
+ matched[msg.sender] = true;
matches[liked].push(msg.sender);
+ matched[liked] = true;
emit Matched(msg.sender, liked);
matchRewards(liked, msg.sender);
}
}
```