DatingDapp

AI First Flight #6
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Impact: medium
Likelihood: medium
Invalid

Like flags are never cleared after a match, enabling duplicate match entries and MultiSig deployments

Root + Impact

Description

  • LikeRegistry is designed so that when two users mutually like each other, they are recorded as a match exactly once and rewarded through a shared MultiSigWallet.

  • After a match is recorded, likes[msg.sender][liked] and likes[liked][msg.sender] are never reset to false. If a user burns and re-mints their profile, the stale like flag persists. A subsequent likeUser call immediately re-triggers the match condition, pushing duplicate entries into both matches arrays and deploying another MultiSigWallet.

if (likes[liked][msg.sender]) {
matches[msg.sender].push(liked);
matches[liked].push(msg.sender);
emit Matched(msg.sender, liked);
@> // likes[msg.sender][liked] and likes[liked][msg.sender] never reset to false
matchRewards(liked, msg.sender);
}

Risk

Likelihood:

  • A user burns their profile with burnProfile(), re-mints it with mintProfile(), and any counterparty who previously liked them calls likeUser again — the stale flag causes an instant duplicate match.
    A user deliberately cycles profiles to farm multiple MultiSig wallets from the same counterparty.

Impact:

  • Multiple MultiSigWallet contracts are deployed for the same user pair, fragmenting ETH rewards across duplicate wallets.

  • matches arrays grow with duplicate entries, corrupting getMatches() output and all front-end match displays.

Proof of Concept

This test walks through the exact burn-and-remint exploit path that causes a duplicate match:

First match — Alice and Bob like each other. A match is recorded and matches[alice] = [bob], matches[bob] = [alice]. Crucially, likes[bob][alice] remains true — it is never cleared.

Alice resets her profile — Alice calls burnProfile() which deletes her profileToToken entry. She then calls mintProfile() and gets a fresh profile with a new token ID. Note that likeRegistry has no concept of this — likes[bob][alice] is still true.

Alice likes Bob again — Alice calls likeUser{value: 1 ether}(bob). The require(!likes[alice][bob]) check passes because likes[alice][bob] was not reset. The mutual-match branch fires again because likes[bob][alice] is still true.

Duplicate match — matches[alice].push(bob) is called a second time. getMatches() for Alice now returns [bob, bob].

To run: forge test --match-test test_duplicateMatchAfterProfileReset -vvvv

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../src/SoulboundProfileNFT.sol";
import "../src/LikeRegistry.sol";
contract M2_DuplicateMatchTest is Test {
SoulboundProfileNFT profileNFT;
LikeRegistry likeRegistry;
address alice = makeAddr("alice");
address bob = makeAddr("bob");
function setUp() public {
profileNFT = new SoulboundProfileNFT();
likeRegistry = new LikeRegistry(address(profileNFT));
vm.deal(alice, 10 ether);
vm.deal(bob, 10 ether);
vm.prank(alice);
profileNFT.mintProfile("Alice", 25, "ipfs://alice");
vm.prank(bob);
profileNFT.mintProfile("Bob", 26, "ipfs://bob");
}
function test_duplicateMatchAfterProfileReset() public {
// Step 1: Alice and Bob match normally
vm.prank(alice);
likeRegistry.likeUser{value: 1 ether}(bob);
vm.prank(bob);
likeRegistry.likeUser{value: 1 ether}(alice); // match recorded
// Step 2: Alice burns and re-mints her profile
// likes[bob][alice] is NEVER cleared — still true
vm.prank(alice);
profileNFT.burnProfile();
vm.prank(alice);
profileNFT.mintProfile("Alice", 25, "ipfs://alice");
// Step 3: Alice likes Bob again — check passes (likes[alice][bob] was not reset after match)
// Immediately hits mutual-match branch because likes[bob][alice] == true (stale)
vm.prank(alice);
likeRegistry.likeUser{value: 1 ether}(bob);
// Step 4: Verify duplicate match entries
vm.prank(alice);
address[] memory aliceMatches = likeRegistry.getMatches();
assertEq(aliceMatches.length, 2, "Alice has duplicate match entries");
assertEq(aliceMatches[0], bob, "First entry is bob");
assertEq(aliceMatches[1], bob, "Second entry is bob — duplicate");
console.log("Alice match count:", aliceMatches.length); // prints 2
}
}

Recommended Mitigation

Reset both like flags immediately after the match is recorded:

if (likes[liked][msg.sender]) {
+ // Clear stale flags to prevent duplicate matches after profile resets
+ likes[msg.sender][liked] = false;
+ likes[liked][msg.sender] = false;
matches[msg.sender].push(liked);
matches[liked].push(msg.sender);
emit Matched(msg.sender, liked);
matchRewards(liked, msg.sender);
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 4 hours ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!