DatingDapp

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

Pending like ETH becomes permanently locked when a liked profile is burned or blocked

Pending Like ETH Becomes Permanently Locked When a Liked Profile Is Burned or Blocked

Description

Users can pay 1 ETH to like another valid profile. When the like is not yet mutual, the ETH remains pending in LikeRegistry until the other user reciprocates and the protocol creates a match.
The issue is that SoulboundProfileNFT allows a profile to be burned by the user or blocked by the owner without notifying LikeRegistry. Once the liked profile is removed, the pending like can no longer be completed because LikeRegistry.likeUser() requires both users to have active profile NFTs. The original 1 ETH remains stuck in LikeRegistry, is not refunded, and is not withdrawable as fees.
// LikeRegistry.sol
function likeUser(address liked) external payable {
require(msg.value >= 1 ether, "Must send at least 1 ETH");
require(!likes[msg.sender][liked], "Already liked");
require(msg.sender != liked, "Cannot like yourself");
require(profileNFT.profileToToken(msg.sender) != 0, "Must have a profile NFT");
require(profileNFT.profileToToken(liked) != 0, "Liked user must have a profile NFT");

// @> ETH is accepted for a pending like
likes[msg.sender][liked] = true;
if (likes[liked][msg.sender]) {
matches[msg.sender].push(liked);
matches[liked].push(msg.sender);
matchRewards(liked, msg.sender);
}

}

// SoulboundProfileNFT.sol
function burnProfile() external {
uint256 tokenId = profileToToken[msg.sender];
require(tokenId != 0, "No profile found");

// @> Profile is removed without clearing or refunding pending likes
_burn(tokenId);
delete profileToToken[msg.sender];
delete _profiles[tokenId];

}

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

// @> Same issue when the owner blocks a profile
_burn(tokenId);
delete profileToToken[blockAddress];
delete _profiles[tokenId];

}
Risk
Likelihood: Medium
This occurs whenever a user pays to like a profile and that liked profile is burned or blocked before reciprocating.

Profile deletion and owner blocking are normal protocol lifecycle actions, so this does not require privileged misuse beyond the intended blockProfile() flow or a user choosing to burn their own profile.

Impact: Medium
The liker’s pending 1 ETH becomes permanently locked in LikeRegistry.

The ETH is not sent to a match multisig, not refunded to the liker, and not withdrawable through withdrawFees() because totalFees was never increased.

Proof of Concept
The following test shows Alice paying 1 ETH to like Bob. Bob then burns his profile before reciprocating. Because Bob no longer has a profile NFT, he cannot complete the mutual match, and Alice’s ETH remains stuck in LikeRegistry.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "forge-std/Test.sol";
import "../src/SoulboundProfileNFT.sol";
import "../src/LikeRegistry.sol";

contract BurnBlockLocksPendingLikeTest is Test {
SoulboundProfileNFT nft;
LikeRegistry registry;

address alice = address(0xA11CE);
address bob = address(0xB0B);
function setUp() public {
nft = new SoulboundProfileNFT();
registry = new LikeRegistry(address(nft));
vm.prank(alice);
nft.mintProfile("Alice", 25, "ipfs://alice");
vm.prank(bob);
nft.mintProfile("Bob", 25, "ipfs://bob");
vm.deal(alice, 1 ether);
vm.deal(bob, 1 ether);
}
function testBurnedProfileLocksPendingLikeETH() public {
vm.prank(alice);
registry.likeUser{value: 1 ether}(bob);
assertEq(address(registry).balance, 1 ether);
vm.prank(bob);
nft.burnProfile();
assertEq(nft.profileToToken(bob), 0);
vm.prank(bob);
vm.expectRevert("Must have a profile NFT");
registry.likeUser{value: 1 ether}(alice);
vm.expectRevert("No fees to withdraw");
registry.withdrawFees();
assertEq(address(registry).balance, 1 ether);
}

}
Recommended Mitigation
Before a profile is burned or blocked, clear all pending likes involving that profile and refund the affected users. One approach is to make SoulboundProfileNFT notify LikeRegistry before deleting a profile.

diff --git a/contracts/SoulboundProfileNFT.sol b/contracts/SoulboundProfileNFT.sol
index abc1234..def5678 100644
--- a/contracts/SoulboundProfileNFT.sol
+++ b/contracts/SoulboundProfileNFT.sol
@@
+interface ILikeRegistry {

  • function cancelPendingLikes(address user) external;
    +}


contract SoulboundProfileNFT is ERC721, Ownable {

  • ILikeRegistry public likeRegistry;


  • function setLikeRegistry(address registry) external onlyOwner {


  • likeRegistry = ILikeRegistry(registry);
  • }


@@
function burnProfile() external {
uint256 tokenId = profileToToken[msg.sender];
require(tokenId != 0, "No profile found");

  • likeRegistry.cancelPendingLikes(msg.sender);

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

  • likeRegistry.cancelPendingLikes(blockAddress);

  • _burn(tokenId);
    delete profileToToken[blockAddress];
    delete _profiles[tokenId];
    }
    LikeRegistry.cancelPendingLikes() should refund pending like ETH for all unresolved likes involving the profile being removed, then clear the corresponding pending like state.

Updates

Lead Judging Commences

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