DatingDapp

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

burnProfile() has no callback to LikeRegistry — all pending likes against a burned profile lock ETH forever

Root + Impact

Description

// SoulboundProfileNFT.sol — burnProfile()
function burnProfile() external {
uint256 tokenId = profileToToken[msg.sender];
require(tokenId != 0, "No profile found");
require(ownerOf(tokenId) == msg.sender, "Not profile owner");
_burn(tokenId);
delete profileToToken[msg.sender]; // @> profile existence deleted
delete _profiles[tokenId];
// @> No call to LikeRegistry to refund pending likes
// @> No event that LikeRegistry listens to
emit ProfileBurned(msg.sender, tokenId);
}
// SoulboundProfileNFT.sol — blockProfile()
function blockProfile(address blockAddress) external onlyOwner {
uint256 tokenId = profileToToken[blockAddress];
require(tokenId != 0, "No profile found");
_burn(tokenId);
delete profileToToken[blockAddress]; // @> same issue for owner-forced burns
delete _profiles[tokenId];
emit ProfileBurned(blockAddress, tokenId);
}
// LikeRegistry.sol — likeUser()
function likeUser(address liked) external payable {
// ...
// @> profileToToken check gates new likes, but not existing ones
require(profileNFT.profileToToken(liked) != 0, "Liked user must have a profile NFT");
// ...
}
// LikeRegistry.sol — NO cancelLike(), NO withdrawBalance() for users
// @> userBalances[A] stays non-zero forever after B burns their profile
// @> The only withdrawal is withdrawFees() — owner-only, touches only totalFees

Risk

Likelihood:

  • Profile deletion is an advertised feature of the protocol — users will use it when leaving the platform, changing wallets, or updating their profile (burn + remint).

  • The owner can blockProfile() any user at any time, immediately triggering this loss for all users who had liked the blocked profile.

  • On a live platform with real users, profile churn is continuous and this loss event will occur regularly.

Impact:

  • Every user who liked a subsequently-deleted or blocked profile permanently loses their 1 ETH stake.

  • Owner-initiated blockProfile() calls function as an involuntary theft mechanism — banning a popular profile with many pending likes destroys significant user funds.

  • Users cannot protect themselves — they have no way to cancel a like or withdraw their balance even knowing the profile will be burned.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../src/LikeRegistry.sol";
import "../src/SoulboundProfileNFT.sol";
contract BurnProfileLocksEthTest is Test {
LikeRegistry registry;
SoulboundProfileNFT nft;
address alice = makeAddr("alice");
address bob = makeAddr("bob");
function setUp() public {
nft = new SoulboundProfileNFT();
registry = new LikeRegistry(address(nft));
vm.prank(alice);
nft.mintProfile("Alice", 25, "ipfs://alice");
vm.prank(bob);
nft.mintProfile("Bob", 27, "ipfs://bob");
deal(alice, 2 ether);
}
function test_burnLocksLiker() public {
// Alice likes Bob, paying 1 ETH
vm.prank(alice);
registry.likeUser{value: 1 ether}(bob);
assertEq(address(registry).balance, 1 ether);
// Bob burns his profile (leaves the platform)
vm.prank(bob);
nft.burnProfile();
// Bob's profile is gone — Alice can never be matched
assertEq(nft.profileToToken(bob), 0);
// Alice's 1 ETH is locked — no cancelLike, no withdrawBalance
// No function exists in LikeRegistry to recover it
assertEq(registry.userBalances(alice), 0); // never credited anyway
assertEq(address(registry).balance, 1 ether); // ETH still trapped
// Attempting to like Bob again fails — profile gone
vm.prank(alice);
vm.expectRevert("Liked user must have a profile NFT");
registry.likeUser{value: 1 ether}(bob);
}
function test_ownerBlockLocksLiker() public {
// Alice likes Bob
vm.prank(alice);
registry.likeUser{value: 1 ether}(bob);
// Owner blocks Bob's profile
nft.blockProfile(bob);
// Same result — Alice's ETH is permanently locked
assertEq(address(registry).balance, 1 ether);
}
}

Recommended Mitigation

Add a cancelLike() function to LikeRegistry that allows a user to withdraw their balance when the liked profile no longer exists:

// LikeRegistry.sol — add cancelLike()
/// @notice Allows a liker to reclaim their ETH if the liked profile has been deleted
function cancelLike(address liked) external {
// @> Only allow cancellation if the liked profile no longer exists
require(profileNFT.profileToToken(liked) == 0, "Profile still active");
require(likes[msg.sender][liked], "No active like");
likes[msg.sender][liked] = false;
uint256 amount = userBalances[msg.sender];
require(amount > 0, "No balance to withdraw");
userBalances[msg.sender] = 0;
(bool success,) = payable(msg.sender).call{value: amount}("");
require(success, "Refund failed");
}
Updates

Lead Judging Commences

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