DatingDapp

AI First Flight #6
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Severity: medium
Valid

blockProfile centralization allows owner to lock user funds permanently

Title: blockProfile centralization allows owner to lock user funds permanently
Severity: Medium
Impact: Owner can block any user, preventing them from matching and locking their ETH.
Likelihood: Medium — requires malicious or compromised owner, but no safeguards exist.
Reference Files: src/SoulboundProfileNFT.sol:57-66, src/LikeRegistry.sol:34-36

Description

The contract owner can call blockProfile on any user, burning their profile NFT. Without a profile, the user cannot call likeUser (which requires profileToToken[msg.sender] != 0), preventing them from matching and recovering their ETH. The vulnerable code:

function blockProfile(address blockAddress) external onlyOwner {
uint256 tokenId = profileToToken[blockAddress];
require(tokenId != 0, "No profile found");
_burn(tokenId);
delete profileToToken[blockAddress];
delete _profiles[tokenId];
}

After blocking, the user loses all platform access. Any ETH they spent on likes or received from matches is trapped — they cannot initiate or complete matches without a profile.

Risk

Impact: Medium. A blocked user who has paid ETH for likes can never match and recover their funds. Their ETH sits in the LikeRegistry contract with no withdrawal mechanism.
Likelihood: Medium. Requires the owner to act maliciously or be compromised. No timelock, multisig, or governance protects against unilateral blocking.
A user who spent 5 ETH liking multiple people is blocked by the owner. They lose their 5 ETH permanently with no recourse and cannot complete any pending matches.

Proof of Concept

function testBlockProfileLocksUserFunds() public {
vm.deal(alice, 5 ether); vm.deal(bob, 5 ether);
vm.prank(alice); nft.mintProfile("Alice", 25, "ipfs://alice");
vm.prank(bob); nft.mintProfile("Bob", 28, "ipfs://bob");
vm.prank(alice); registry.likeUser{value: 1 ether}(bob);
// Owner blocks Bob — Bob can no longer match
vm.prank(owner); nft.blockProfile(bob);
vm.prank(bob);
vm.expectRevert("Must have a profile NFT");
registry.likeUser{value: 1 ether}(alice); // Bob can't reciprocate
// Alice's 1 ETH is locked — Bob can never match with her
assertEq(address(registry).balance, 1 ether);
}

Owner blocks Bob, preventing him from completing the mutual match — Alice's 1 ETH is permanently locked.

Recommended Mitigation

Add a withdrawal mechanism for users to recover their ETH when blocked, or implement a timelock/governance for blockProfile:

function withdrawIfBlocked() external {
require(profileToToken[msg.sender] == 0 && isBlocked[msg.sender], "Not blocked");
uint256 amount = userBalances[msg.sender];
userBalances[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}

This allows blocked users to recover their contributed ETH, preventing permanent fund lock while maintaining the owner's ability to moderate the platform.

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 1 day ago
Submission Judgement Published
Validated
Assigned finding tags:

[M-03] App owner can have users' funds locked by blocking them

## 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.

Support

FAQs

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

Give us feedback!