DatingDapp

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

[M-04] `blockProfile` Stale State — Blocked Users Exploit Pre-Block Likes After Re-Mint

[M-04] blockProfile Stale State — Blocked Users Exploit Pre-Block Likes After Re-Mint

Scope

  • SoulboundProfileNFT.sol

  • LikeRegistry.sol

Description

blockProfile() burns the user's NFT and clears profileToToken, but does NOT clean up likes[] mappings in LikeRegistry. A blocked user can re-mint a new profile, and their old likes persist — enabling match exploitation using stale pre-block data.

function blockProfile(address blockAddress) external onlyOwner {
_burn(tokenId);
delete profileToToken[blockAddress];
delete _profiles[tokenId];
@> // likes[blockAddress][...] in LikeRegistry NOT cleared
}

Risk

Likelihood: Medium — Requires blocked user to re-mint and another user to like them.

Impact: Medium — Bypasses owner's moderation. Blocked user regains matches and potential rewards using stale, pre-block like data.

Severity: Medium

  • SWC: SWC-124 (Write to Arbitrary Storage Location — inverse: failure to clear state)

  • CWE: CWE-459 (Incomplete Cleanup)

  • Evidence Grade: A

Proof of Concept

Alice likes Bob, owner blocks Alice, Alice re-mints profile, Bob likes Alice — mutual match triggers using Alice's stale pre-block like.

function test_FINDING008_blockProfile_stale_state() public {
vm.prank(alice); registry.likeUser{value: 1 ether}(bob);
assertTrue(registry.likes(alice, bob));
vm.prank(owner); nft.blockProfile(alice);
vm.prank(alice); nft.mintProfile("Alice-New", 25, "ipfs://new");
vm.prank(bob); registry.likeUser{value: 1 ether}(alice);
vm.prank(bob);
assertEq(registry.getMatches().length, 1, "Match via stale like");
}

forge test --match-test test_FINDING008_blockProfile_stale_state -vvvvPASS

Recommended Mitigation

The cross-contract state inconsistency occurs because SoulboundProfileNFT has no way to notify LikeRegistry when a profile is burned or blocked. Adding a per-user epoch counter that increments on every mint invalidates all prior likes without requiring explicit cleanup. The mutual-like check then verifies that both users' epochs match their stored like epoch.

// LikeRegistry.sol
+mapping(address => uint256) public userEpoch;
+
+function incrementEpoch(address user) external {
+ require(msg.sender == address(profileNFT), "Only NFT contract");
+ userEpoch[user]++;
+}
+
mapping(address => mapping(address => bool)) public likes;
+mapping(address => mapping(address => uint256)) public likeEpoch;
function likeUser(address liked) external payable {
// ... existing checks ...
likes[msg.sender][liked] = true;
+ likeEpoch[msg.sender][liked] = userEpoch[msg.sender];
// ...
if (likes[liked][msg.sender]
+ && likeEpoch[liked][msg.sender] == userEpoch[liked]) {
// mutual match
}
}
// SoulboundProfileNFT.sol
+LikeRegistry public likeRegistry;
+
function blockProfile(address blockAddress) external onlyOwner {
// ... existing burn logic ...
+ likeRegistry.incrementEpoch(blockAddress);
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 2 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!