Root + Impact
Description
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];
delete _profiles[tokenId];
emit ProfileBurned(msg.sender, tokenId);
}
function blockProfile(address blockAddress) external onlyOwner {
uint256 tokenId = profileToToken[blockAddress];
require(tokenId != 0, "No profile found");
_burn(tokenId);
delete profileToToken[blockAddress];
delete _profiles[tokenId];
emit ProfileBurned(blockAddress, tokenId);
}
function likeUser(address liked) external payable {
require(profileNFT.profileToToken(liked) != 0, "Liked user must have a profile NFT");
}
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
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 {
vm.prank(alice);
registry.likeUser{value: 1 ether}(bob);
assertEq(address(registry).balance, 1 ether);
vm.prank(bob);
nft.burnProfile();
assertEq(nft.profileToToken(bob), 0);
assertEq(registry.userBalances(alice), 0);
assertEq(address(registry).balance, 1 ether);
vm.prank(alice);
vm.expectRevert("Liked user must have a profile NFT");
registry.likeUser{value: 1 ether}(bob);
}
function test_ownerBlockLocksLiker() public {
vm.prank(alice);
registry.likeUser{value: 1 ether}(bob);
nft.blockProfile(bob);
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:
function cancelLike(address liked) external {
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");
}