DatingDapp

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

M-03: Owner can lock users’ pending like deposits by blocking their profile

M-03: Owner can lock users’ pending like deposits by blocking their profile

Severity: Medium

Summary

The app owner can call SoulboundProfileNFT::blockProfile() on any user at any time. If that user has already paid ETH into LikeRegistry through likeUser() but has not yet completed a mutual match, their funds can become permanently stuck.

This happens because blocking removes the user’s profile NFT, and LikeRegistry requires both parties to have an active profile NFT before a like can be made. Once blocked, the user can no longer complete future matches, and there is no refund or withdrawal path for their pending balance.


Vulnerability Details

SoulboundProfileNFT::blockProfile() lets the owner forcibly remove any user profile:

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

Meanwhile, LikeRegistry::likeUser() requires both the caller and the liked user to hold profile NFTs:

require(profileNFT.profileToToken(msg.sender) != 0, "Must have a profile NFT");
require(profileNFT.profileToToken(liked) != 0, "Liked user must have a profile NFT");

If a user already deposited ETH through likeUser() and is then blocked by the owner:

  • their profile is removed

  • they can no longer like anyone again

  • others can no longer like them

  • any pending deposits associated with them can no longer progress toward a match

  • there is no withdrawal or refund function for those pending deposits

So the owner has the power to freeze user participation in a way that can strand deposited ETH inside LikeRegistry.


Impact

The owner can arbitrarily cause users’ pending funds to become inaccessible.

This breaks an important fairness assumption of the protocol:

  • users may deposit ETH in good faith

  • the owner can unilaterally prevent those deposits from ever being used for a match

  • there is no recovery path for the affected user

This is especially relevant because the project’s core flow is built around pre-paying for a future mutual match. If admin moderation can permanently disable a user without refunding their pending balance, users face custodial loss risk.


Root Cause

The protocol couples profile existence to match eligibility, but does not provide any mechanism to unwind or refund pending user funds when an admin removes a profile.


Internal Preconditions

  • LikeRegistry holds user-deposited ETH for unmatched likes

  • there is no refund or cancel function for pending deposits

  • owner has permission to call blockProfile()

External Preconditions

  • a user has deposited ETH through likeUser()

  • the user is blocked before those funds are consumed in a match


Attack / Failure Path

  1. Alice mints a profile NFT.

  2. Bob mints a profile NFT.

  3. Alice calls likeUser(Bob) and deposits ETH.

  4. Before Bob reciprocates, the owner calls blockProfile(Alice).

  5. Alice’s profile NFT is burned.

  6. Alice can no longer like others.

  7. Bob can no longer call likeUser(Alice) because Alice no longer has a valid profile NFT.

  8. Alice’s pending deposit remains stuck in LikeRegistry with no refund path.


Proof of Concept

This PoC assumes likeUser() correctly credits userBalances[msg.sender] += msg.value, since otherwise H-01 masks the issue.

function test_OwnerCanLockUserFundsByBlockingProfile() public {
address alice = address(0xA11CE);
address bob = address(0xB0B);
SoulboundProfileNFT nft = new SoulboundProfileNFT();
LikeRegistry registry = new LikeRegistry(address(nft));
// Mint profiles
vm.prank(alice);
nft.mintProfile("Alice", 25, "ipfs://alice");
vm.prank(bob);
nft.mintProfile("Bob", 26, "ipfs://bob");
// Alice deposits funds by liking Bob
vm.deal(alice, 1 ether);
vm.prank(alice);
registry.likeUser{value: 1 ether}(bob);
// Assuming H-01 is fixed:
assertEq(registry.userBalances(alice), 1 ether);
// Owner blocks Alice
nft.blockProfile(alice);
assertEq(nft.profileToToken(alice), 0);
// Bob can no longer complete the match
vm.deal(bob, 1 ether);
vm.prank(bob);
vm.expectRevert("Liked user must have a profile NFT");
registry.likeUser{value: 1 ether}(alice);
// Alice's funds remain stuck in registry with no withdrawal path
assertEq(registry.userBalances(alice), 1 ether);
}

PoC Explanation

This test shows that:

  • Alice successfully deposits ETH into the system

  • the owner removes Alice’s profile

  • Bob can no longer reciprocate the like

  • Alice’s recorded funds remain in LikeRegistry

  • there is no mechanism for Alice to recover them

So the owner can lock pending user funds by blocking the user.


Recommendation

There are a few reasonable fixes, depending on intended protocol behavior:

Option 1: Refund on block
When a user is blocked, add logic in LikeRegistry to return their unmatched balance.

Option 2: Add user withdrawal / cancel path
Allow users to withdraw unmatched deposits if they are blocked or if a match never occurs.

Option 3: Separate moderation from funds custody
Do not let blocking immediately destroy the ability to settle or recover pending balances. For example, mark a profile as blocked for future participation, but preserve a way to unwind open financial state.

Option 4: Pair-specific deposit accounting
Track deposits per pair and allow forced settlement/refund when one side is blocked.

A minimal mitigation direction would be:

  • add a blocked-user status instead of directly destroying financial reachability

  • add a refund function for unmatched balances

  • ensure admin moderation cannot strand user funds

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!