DatingDapp

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

Centralized owner can arbitrarily block users, causing permanent loss of user funds

Centralized owner can arbitrarily block users, causing permanent loss of user funds

Description

The SoulboundProfileNFT::blockProfile function allows the contract owner to block any user at any time without restriction. When a user is blocked, their profile NFT is burned and they lose access to all ETH they have spent on likes in LikeRegistry. There is no refund mechanism and no way for the user to recover their funds.

@> 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);
@> // No refund of user's ETH from LikeRegistry
}

Risk

Likelihood:

  • Owner has unrestricted ability to call SoulboundProfileNFT::blockProfile on any address with a profile NFT

Impact:

  • Users permanently lose all ETH spent on unmatched likes (1 ETH per like)

  • A malicious or compromised owner could grief high-value users who have spent significant ETH on the platform

  • Users have no recourse or appeal mechanism

Proof of Concept

function testOwnerCanBlockAndUserLosesFunds() public {
// Setup: Create profiles for Alice, Bob, Charlie, Dave, Eve
vm.prank(alice);
soulboundNFT.mintProfile("Alice", 25, "ipfs://alice");
vm.prank(bob);
soulboundNFT.mintProfile("Bob", 28, "ipfs://bob");
vm.prank(charlie);
soulboundNFT.mintProfile("Charlie", 30, "ipfs://charlie");
vm.prank(dave);
soulboundNFT.mintProfile("Dave", 27, "ipfs://dave");
vm.prank(eve);
soulboundNFT.mintProfile("Eve", 26, "ipfs://eve");
// Alice spends 4 ETH liking various users (no matches yet)
vm.deal(alice, 4 ether);
vm.startPrank(alice);
likeRegistry.likeUser{value: 1 ether}(bob);
likeRegistry.likeUser{value: 1 ether}(charlie);
likeRegistry.likeUser{value: 1 ether}(dave);
likeRegistry.likeUser{value: 1 ether}(eve);
vm.stopPrank();
// Verify ETH is in LikeRegistry
assertEq(address(likeRegistry).balance, 4 ether);
// Owner blocks Alice before any matches occur
vm.prank(owner);
soulboundNFT.blockProfile(alice);
// Alice has no profile, can't receive likes back, can't match
assertEq(soulboundNFT.profileToToken(alice), 0);
// The 4 ETH is permanently stuck in LikeRegistry with no way to recover
assertEq(address(likeRegistry).balance, 4 ether);
}

Note: This issue compounds with the userBalances bug in LikeRegistry::likeUser where userBalances[msg.sender] is never updated. Even if a refund mechanism existed, the accounting is broken so users would receive nothing. The ETH is permanently stuck in the contract regardless.

Recommended Mitigation

Consider implementing one or more of the following:

Add a refund mechanism to LikeRegistry:

+ function refundUser(address user) external {
+ uint256 balance = userBalances[user];
+ require(balance > 0, "No balance to refund");
+
+ userBalances[user] = 0;
+
+ (bool success,) = payable(user).call{value: balance}("");
+ require(success, "Refund failed");
+ }

Update SoulboundProfileNFT::blockProfile to refund before blocking:

function blockProfile(address blockAddress) external onlyOwner {
uint256 tokenId = profileToToken[blockAddress];
require(tokenId != 0, "No profile found");
+ // Refund user's balance from LikeRegistry before blocking
+ uint256 userBalance = likeRegistry.userBalances(blockAddress);
+ if (userBalance > 0) {
+ likeRegistry.refundUser(blockAddress);
+ }
+
_burn(tokenId);
delete profileToToken[blockAddress];
delete _profiles[tokenId];
emit ProfileBurned(blockAddress, tokenId);
}

Alternatively, add a timelock or multi-sig requirement for blocking users, giving them time to withdraw or match before the block takes effect.

Updates

Lead Judging Commences

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