DatingDapp

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

Owner Can Permanently Lock User Funds by Blocking Their Profile

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");                                                                                                                           
    
  • }

Updates

Lead Judging Commences

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