DatingDapp

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

No refund or unlike path: a liker's >= 1 ETH stake (and any excess over 1 ETH) is unrecoverable if the match never happens

Description

Normal behavior: liking is a bet on a mutual match. A user who likes one or more people but never matches should be able to recover the ETH they staked, and a user should never lose more than the amount they intended to stake per like. In a healthy design there is an unlike/refund for pending likes and the like amount is exact (or the surplus is returned).

Specific issue: LikeRegistry has NO exit path for a liker's funds outside of a successful mutual match. There is no unlike, no refund, and no per-user withdraw function anywhere in the contract; the only ETH-out functions are matchRewards (internal, only on a mutual match) and withdrawFees (owner, only the accrued fees). This creates three distinct, permanent losses, none of which is fixed by simply adding the missing credit from finding #1:

  1. Never-matched stake is stuck. A user likes someone who never likes them back. Their >= 1 ETH sits in the contract with no way to reclaim it. Since most likes on a dating app never become mutual, this is the common case, not an edge case.

  2. Liked user disappears. If the liked user burns their profile (or is blocked by the owner) before reciprocating, the mutual match can never occur, so the liker's stake is locked forever with certainty.

  3. Overpayment is lost outright. likeUser only enforces a >= 1 ether FLOOR — there is no cap and no refund of the surplus. A user who sends 2 ETH (or fat-fingers 50 ETH) for a single like loses everything above the intended stake, because the excess is never tracked or returned.

The natural user workaround — burn the profile and re-mint to "reset" — does not free the funds either, and is additionally blocked from re-liking by the stale-state issue in finding #3. So once ETH enters likeUser, the only way it ever leaves is a mutual match; every other path is a permanent loss.

function likeUser(address liked) external payable {
require(msg.value >= 1 ether, "Must send at least 1 ETH"); // @> FLOOR only: no cap, no refund of surplus
...
likes[msg.sender][liked] = true;
// @> no unlike / no refund / no per-user withdraw exists anywhere in the contract
}

Risk

Likelihood:

  • The majority of likes never become mutual matches, so unrecoverable stakes are the norm, not an edge case.

  • The liked user burning/being-blocked before reciprocating is a normal lifecycle event.

  • Overpaying (or sending a round number > 1 ETH) is an easy and irreversible user mistake.

Impact:

  • Permanent loss of every stake that does not result in a match (the common outcome), permanent loss when a counterparty leaves before matching, and total loss of any amount sent above 1 ETH per like. Because there is no withdraw path at all, these funds are irrecoverable by the user, the counterparty, or even the owner.

Proof of Concept

test/LikeRegistryAudit.t.sol::testExcessValueLockedNoRefund (PASS): Alice sends 50 ETH in one likeUser call and loses all 50 (userBalances(alice)==0, registry.balance==50 ETH). test/SeamAudit.t.sol::test_F3_likedUserBurns_likerEthStuckNoRecovery (PASS): the liked user burns their profile; the liker's 1 ETH is stuck with no recovery.

Recommended Mitigation

Add an unlike/refund for pending (un-matched) likes and a per-user withdraw; refund msg.value above the required stake, or require exactly the stake amount.

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!