DatingDapp

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

No Withdraw Function for Non-Matched Users

Root + Impact

Description

  • When a user likes someone who does not reciprocate, the user should be able to reclaim their deposited ETH through a withdrawal function, either immediately or after a timeout period.

  • The LikeRegistry contract has no withdrawal function for users. Once ETH is sent via `likeUser()`, the only path for those funds is through `matchRewards()` on a mutual like. Non-reciprocated likes result in permanent fund lock with no recovery mechanism.

// LikeRegistry.sol - Available functions for fund movement:
// ✓ Receives ETH from users
function likeUser(address liked) external payable { ... }
// ✓ Distributes funds on match (internal only)
function matchRewards(address from, address to) internal { ... }
// ✓ Owner withdraws protocol fees
function withdrawFees() external onlyOwner { ... }
// ✓ Accepts raw ETH transfers
receive() external payable {}
@> // ❌ MISSING: No function for users to withdraw their balance
@> // function withdrawBalance() external { ... }

Risk

Likelihood:

  • Non-reciprocated likes are a common occurrence in dating applications - most likes do not result in matches

  • Every user who likes someone that doesn't like them back is affected

Impact:

  • Users permanently lose all ETH deposited for non-reciprocated likes

  • No timeout or expiry mechanism exists to eventually release funds

  • Combined with F-001, even matched users cannot access their funds

  • Creates significant financial risk for any user of the protocol

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../../src/SoulboundProfileNFT.sol";
import "../../src/LikeRegistry.sol";
/**
* @title F-002 PoC: No Withdraw Function for Non-Matched Users
* @notice Demonstrates that users who like someone but aren't liked back
* have no way to reclaim their ETH
*/
contract F002_NoWithdrawFunctionTest is Test {
SoulboundProfileNFT public profileNFT;
LikeRegistry public likeRegistry;
address alice = makeAddr("alice");
address bob = makeAddr("bob");
address charlie = makeAddr("charlie");
function setUp() public {
profileNFT = new SoulboundProfileNFT();
likeRegistry = new LikeRegistry(address(profileNFT));
vm.deal(alice, 10 ether);
vm.deal(bob, 10 ether);
vm.deal(charlie, 10 ether);
// Create profiles
vm.prank(alice);
profileNFT.mintProfile("Alice", 25, "ipfs://alice");
vm.prank(bob);
profileNFT.mintProfile("Bob", 27, "ipfs://bob");
vm.prank(charlie);
profileNFT.mintProfile("Charlie", 30, "ipfs://charlie");
}
/**
* @notice Proves non-matched users cannot withdraw their ETH
*/
function testNonMatchedUserCannotWithdraw() public {
// Alice likes Bob with 1 ETH
vm.prank(alice);
likeRegistry.likeUser{value: 1 ether}(bob);
// Bob never likes Alice back
// Alice's ETH is in the contract
assertEq(address(likeRegistry).balance, 1 ether);
// There is NO function for Alice to withdraw
// Even if userBalances was properly tracked, there's no withdraw()
// Attempt to find any way to get ETH back:
// 1. withdrawFees() - only for owner, only withdraws totalFees
vm.prank(alice);
vm.expectRevert(); // Not owner
likeRegistry.withdrawFees();
// 2. No withdraw() or reclaim() function exists
// 3. No timeout/expiry mechanism
// 4. No refund mechanism
// Alice's 1 ETH is permanently locked
assertEq(address(likeRegistry).balance, 1 ether, "ETH still locked");
}
/**
* @notice Proves multiple non-reciprocated likes accumulate locked funds
*/
function testMultipleNonMatchedLikesAccumulateLocked() public {
// Alice likes Bob
vm.prank(alice);
likeRegistry.likeUser{value: 1 ether}(bob);
// Alice likes Charlie
vm.prank(alice);
likeRegistry.likeUser{value: 1 ether}(charlie);
// Bob likes Charlie (not Alice)
vm.prank(bob);
likeRegistry.likeUser{value: 1 ether}(charlie);
// 3 ETH locked, no matches, no way to recover
assertEq(address(likeRegistry).balance, 3 ether);
// None of these users can get their ETH back
}
/**
* @notice Shows the intended behavior would require a withdraw function
*/
function testExpectedWithdrawFunctionMissing() public {
// Lock some ETH
vm.prank(alice);
likeRegistry.likeUser{value: 1 ether}(bob);
// Expected function that should exist but doesn't:
// function withdrawBalance() external {
// uint256 balance = userBalances[msg.sender];
// require(balance > 0, "No balance");
// userBalances[msg.sender] = 0;
// payable(msg.sender).transfer(balance);
// }
// Verify no such function exists by checking contract interface
// (This test passes because the function doesn't exist)
}
}

Recommended Mitigation

+ function withdrawBalance() external {
+ uint256 balance = userBalances[msg.sender];
+ require(balance > 0, "No balance to withdraw");
+
+ userBalances[msg.sender] = 0;
+
+ (bool success,) = payable(msg.sender).call{value: balance}("");
+ require(success, "Transfer failed");
+ }
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!