DatingDapp

AI First Flight #6
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Impact: medium
Likelihood: medium
Invalid

`burnProfile()` can strand pending like funds by making the match flow uncompletable

Severity

Medium

Likelihood

Medium

Root + Impact

burnProfile() removes an active profile without resolving its pending like funds, leaving them locked in LikeRegistry.

Description

  • The expected behavior is that a user who has already paid for a like should either be able to complete the match flow or recover the pending funds before deleting their profile.

  • Instead, burnProfile() deletes the profile immediately, while LikeRegistry keeps the paid-like state and balance tied to the address. After that, the counterparty can no longer complete the mutual match because the liked user no longer has a profile, and the payer has no refund path to recover the deposited ETH.

function burnProfile() external {
uint256 tokenId = profileToToken[msg.sender];
require(tokenId != 0, "No profile found");
require(ownerOf(tokenId) == msg.sender, "Not profile owner");
@> _burn(tokenId);
@> delete profileToToken[msg.sender];
delete _profiles[tokenId];
}
function likeUser(address liked) external payable {
require(msg.value >= 1 ether, "Must send at least 1 ETH");
...
@> userBalances[msg.sender] += msg.value;
...
require(profileNFT.profileToToken(liked) != 0, "Liked user must have a profile NFT");
}

Risk

Likelihood: Medium

  • This occurs when a user pays for a like and then burns their profile before the counterparty sends the mutual like.

  • The state transition is reachable through normal user actions and does not require privileged access or unusual conditions.

Impact: Medium

  • Pending user funds can become irrecoverable because the mutual match can no longer be completed after the liked profile is deleted.

  • The protocol loses a valid path to either distribute or refund paid-like ETH, disrupting the intended fund flow.

Proof of Concept

The following test shows that after wallet_A pays for a like and burns the profile, wallet_B can no longer complete the mutual match because wallet_A no longer has a profile. The paid 1 ether remains credited in LikeRegistry, and no user-accessible recovery path exists.

function test_burnProfile_canStrandPendingLikeFunds() public {
address wallet_A = makeAddr("Wallet_A");
address wallet_B = makeAddr("Wallet_B");
deal(wallet_A, 2 ether);
deal(wallet_B, 1 ether);
vm.prank(wallet_A);
soulboundNFT.mintProfile("Wallet_A", 25, "ipfs://Wallet_A");
vm.prank(wallet_B);
soulboundNFT.mintProfile("Wallet_B", 35, "ipfs://Wallet_B");
// wallet_A pays for a like.
vm.prank(wallet_A);
registry.likeUser{value: 1 ether}(wallet_B);
// wallet_A removes the profile before wallet_B responds.
vm.prank(wallet_A);
soulboundNFT.burnProfile();
// The paid balance is still stored in the registry.
assertEq(registry.userBalances(wallet_A), 1 ether);
assertEq(soulboundNFT.profileToToken(wallet_A), 0);
// wallet_B can no longer complete the mutual match.
vm.prank(wallet_B);
vm.expectRevert("Liked user must have a profile NFT");
registry.likeUser{value: 1 ether}(wallet_A);
}

Recommended Mitigation

Prevent profile deletion while the user still has pending funds recorded in LikeRegistry. This ensures a user cannot burn the profile before the paid-like flow has been
resolved.

+ interface IRegistry {
+ function userHasBalance(address user) external view returns (bool);
+ }
contract SoulboundProfileNFT is ERC721, Ownable {
+ IRegistry public registry;
+
+ function setRegistry(address _registry) external onlyOwner {
+ registry = IRegistry(_registry);
+ }
function burnProfile() external {
uint256 tokenId = profileToToken[msg.sender];
require(tokenId != 0, "No profile found");
require(ownerOf(tokenId) == msg.sender, "Not profile owner");
+ require(!registry.userHasBalance(msg.sender), "Pending balance exists");
_burn(tokenId);
delete profileToToken[msg.sender];
delete _profiles[tokenId];
emit ProfileBurned(msg.sender, tokenId);
}
}

A corresponding helper can be added to LikeRegistry:

+ function userHasBalance(address user) external view returns (bool) {
+ return userBalances[user] > 0;
` }
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 2 hours ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!