blockProfile Removes User's Ability to Interact With LikeRegistry, Permanently Locking Their ETH
Description
Users send 1 ETH per like to LikeRegistry, with funds held in the contract until a mutual match pools them into a MultiSigWallet. The protocol provides no
withdrawal function for users to reclaim unmatched ETH.
When the owner calls blockProfile on a user, their NFT is burned and profileToToken[user] is deleted. Since likeUser requires profileNFT.profileToToken(msg.sender)
!= 0, the blocked user can no longer interact with LikeRegistry at all. Any ETH they had accumulated for pending likes is permanently locked — neither they nor the
owner can recover it.
function blockProfile(address blockAddress) external onlyOwner {
uint256 tokenId = profileToToken[blockAddress];
require(tokenId != 0, "No profile found");
_burn(tokenId);
@> delete profileToToken[blockAddress]; // user loses ability to call likeUser
delete _profiles[tokenId];
emit ProfileBurned(blockAddress, tokenId);
}
// LikeRegistry.sol
function likeUser(address liked) external payable {
// ...
@> require(profileNFT.profileToToken(msg.sender) != 0, "Must have a profile NFT"); // blocked user fails here forever
// ...
}
@> // No withdrawal function exists for users to reclaim their ETH
function withdrawFees() external onlyOwner { // only owner, only protocol fees
require(totalFees > 0, "No fees to withdraw");
// ...
}
Risk
Likelihood:
Every time the owner blocks a user who has outstanding unmatched likes, those users' ETH becomes permanently unrecoverable — this is an inevitable consequence of
routine moderation.
Users who liked someone and are waiting for a mutual match lose their funds the moment the owner blocks them, with no warning or refund.
Impact:
Blocked users permanently lose all ETH held in LikeRegistry with zero recourse — no withdrawal, no rescue path, no timeout mechanism.
Users who liked a blocked person also lose their ETH — their liked address now has no profile, so a mutual match can never be triggered.
Proof of Concept
function test_M03_BlockProfileLocksUserFunds() public {
vm.deal(bob, 10 ether);
vm.deal(alice, 10 ether);
vm.prank(bob); soulboundNFT.mintProfile("Bob", 25, "ipfs://bob");
vm.prank(alice); soulboundNFT.mintProfile("Alice", 25, "ipfs://alice");
// Alice likes Bob — 1 ETH locked in LikeRegistry
vm.prank(alice);
likeRegistry.likeUser{value: 1 ether}(bob);
// Owner blocks Bob
vm.prank(owner);
soulboundNFT.blockProfile(bob);
assertEq(soulboundNFT.profileToToken(bob), 0);
// Bob can no longer interact with LikeRegistry
vm.prank(bob);
vm.expectRevert("Must have a profile NFT");
likeRegistry.likeUser{value: 1 ether}(alice);
// Alice's 1 ETH is permanently locked — Bob has no profile, match is impossible
assertEq(address(likeRegistry).balance, 1 ether);
}
Recommended Mitigation
// SoulboundProfileNFT.sol
function blockProfile(address blockAddress) external onlyOwner {
uint256 tokenId = profileToToken[blockAddress];
require(tokenId != 0, "No profile found");
_burn(tokenId);
delete profileToToken[blockAddress];
delete _profiles[tokenId];
isBlocked[blockAddress] = true;
emit ProfileBurned(blockAddress, tokenId);
}
// LikeRegistry.sol
function withdrawBalance() external {
require(profileNFT.isBlocked(msg.sender) || profileNFT.profileToToken(msg.sender) == 0,
"Active profile — use cancelLike");
uint256 amount = userBalances[msg.sender];
require(amount > 0, "Nothing to withdraw");
userBalances[msg.sender] = 0;
(bool success,) = payable(msg.sender).call{value: amount}("");
require(success, "Transfer failed");
}
## Description App owner can block users at will, causing users to have their funds locked. ## Vulnerability Details `SoulboundProfileNFT::blockProfile` can block any app's user at will. ```js /// @notice App owner can block users 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); } ``` ## Proof of Concept The following code demonstrates the scenario where the app owner blocks `bob` and he is no longer able to call `LikeRegistry::likeUser`. Since the contract gives no posibility of fund withdrawal, `bob`'s funds are now locked. Place `test_blockProfileAbuseCanCauseFundLoss` in `testSoulboundProfileNFT.t.sol`: ```js function test_blockProfileAbuseCanCauseFundLoss() public { vm.deal(bob, 10 ether); vm.deal(alice, 10 ether); // mint a profile NFT for bob vm.prank(bob); soulboundNFT.mintProfile("Bob", 25, "ipfs://profileImage"); // mint a profile NFT for Alice vm.prank(alice); soulboundNFT.mintProfile("Alice", 25, "ipfs://profileImage"); // alice <3 bob vm.prank(alice); likeRegistry.likeUser{value: 1 ether}(bob); vm.startPrank(owner); soulboundNFT.blockProfile(bob); assertEq(soulboundNFT.profileToToken(msg.sender), 0); vm.startPrank(bob); vm.expectRevert("Must have a profile NFT"); // bob is no longer able to like a user, as his profile NFT is deleted // his funds are effectively locked likeRegistry.likeUser{value: 1 ether}(alice); } ``` And run the test: ```bash $ forge test --mt test_blockProfileAbuseCanCauseFundLoss Ran 1 test for test/testSoulboundProfileNFT.t.sol:SoulboundProfileNFTTest [PASS] test_blockProfileAbuseCanCauseFundLoss() (gas: 326392) Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.42ms (219.63µs CPU time) Ran 1 test suite in 140.90ms (1.42ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests) ``` ## Impact App users can have their funds locked, as well as miss out on potential dates. ## Recommendations Add a voting mechanism to prevent abuse and/or centralization of the feature.
The contest is live. Earn rewards by submitting a finding.
Submissions are being reviewed by our AI judge. Results will be available in a few minutes.
View all submissionsThe contest is complete and the rewards are being distributed.