DatingDapp

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

Owner can call blockProfile() on any user at any time, burning their NFT and permanently locking any pending ETH they have committed to likes

Root + Impact

Description

  • likeUser() requires the caller to hold an active profile NFT (profileToToken(msg.sender) != 0). Users send at least 1 ETH per like call, and those funds are held in LikeRegistry pending a mutual match.

  • blockProfile() is callable by the owner at any time on any address. It burns the target's NFT and deletes their profile entry, with no time delay, no warning, and no recourse. After blocking, the victim's profile NFT is gone — they cannot call likeUser() — but any ETH they deposited in pending (unmatched) likes remains locked in the contract with no withdrawal path.

// SoulboundProfileNFT.sol L57-66
function blockProfile(address blockAddress) external onlyOwner { // @> unrestricted, instant
uint256 tokenId = profileToToken[blockAddress];
require(tokenId != 0, "No profile found");
_burn(tokenId);
delete profileToToken[blockAddress]; // @> NFT gone — likeUser() now reverts for this address
delete _profiles[tokenId];
emit ProfileBurned(blockAddress, tokenId);
}
// LikeRegistry.sol L35
require(profileNFT.profileToToken(msg.sender) != 0, "Must have a profile NFT"); // @> permanently fails after block

Risk

Likelihood:

  • Requires the owner to act maliciously or arbitrarily against a specific user — not a passive bug, but an unguarded admin power with no check on timing or fund state.

  • Becomes more likely as the platform grows and disputes between users and the platform operator arise.

Impact:

  • Any ETH the blocked user committed to pending likes (likes that have not yet been mutually returned) is permanently stranded — there is no withdrawal function for users, and withdrawFees() only covers the collected fee pool, not user balances.

  • The user loses both their profile and their funds simultaneously with no on-chain appeal path.

Proof of Concept

Alice likes Bob and sends 1 ETH. Owner blocks Bob. Bob can no longer like anyone (NFT burned). Alice's 1 ETH for the pending Bob-like is now locked in LikeRegistry with no way to recover it.

function test_blockProfileAbuseCanCauseFundLoss() public {
vm.prank(bob); soulboundNFT.mintProfile("Bob", 25, "ipfs://profileImage");
vm.prank(alice); soulboundNFT.mintProfile("Alice", 25, "ipfs://profileImage");
vm.prank(alice); likeRegistry.likeUser{value: 1 ether}(bob);
vm.prank(owner); soulboundNFT.blockProfile(bob);
// bob cannot like anyone — profile burned
vm.prank(bob);
vm.expectRevert("Must have a profile NFT");
likeRegistry.likeUser{value: 1 ether}(alice);
}

Alice's 1 ETH remains in the contract unreachable by either party after the block.

Recommended Mitigation

Add a withdrawal function so users can reclaim their pending like deposits, and require a governance or time-locked process before blocking takes effect.

+ mapping(address => uint256) public pendingWithdrawals;
function likeUser(address liked) external payable {
...
+ pendingWithdrawals[msg.sender] += msg.value;
if (likes[liked][msg.sender]) {
+ pendingWithdrawals[msg.sender] -= userBalances[msg.sender]; // consumed on match
matchRewards(liked, msg.sender);
}
}
+ function withdrawPending() external {
+ uint256 amount = pendingWithdrawals[msg.sender];
+ require(amount > 0, "Nothing to withdraw");
+ pendingWithdrawals[msg.sender] = 0;
+ (bool ok,) = payable(msg.sender).call{value: amount}("");
+ require(ok, "Transfer failed");
+ }

Additionally, consider a time-locked or multisig-governed block mechanism, or at minimum require that any pending balances for the blocked address are refunded at the time of blocking.

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!