DatingDapp

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

likeUser() uses >= instead of == — overpaid ETH is permanently locked with no refund path

Root + Impact

Description

The issue

The payment check uses >= instead of ==:

require(msg.value >= 1 ether, "Must send at least 1 ETH");

Any ETH sent above 1 ether is silently accepted into the contract. The userBalances[msg.sender] mapping (once the missing-credit bug is fixed) would credit the full msg.value — but since the fee math and match pool are designed around 1 ETH per user, any excess distorts the pool accounting. More critically in the current codebase where userBalances is never written, the entire msg.value is simply locked. There is no withdrawExcess(), no partial-refund logic, and no user-facing withdrawal function in LikeRegistry. The only withdrawal is withdrawFees() for the owner, which only touches totalFees.

// LikeRegistry.sol — likeUser()
function likeUser(address liked) external payable {
// @> >= allows any amount >= 1 ETH — overpayment is silently swallowed
require(msg.value >= 1 ether, "Must send at least 1 ETH");
// @> No refund of excess: msg.value - 1 ether is never returned
// @> No userBalances write anywhere in this function
// @> No withdrawal function available to users
likes[msg.sender][liked] = true;
emit Liked(msg.sender, liked);
if (likes[liked][msg.sender]) {
matches[msg.sender].push(liked);
matches[liked].push(msg.sender);
emit Matched(msg.sender, liked);
matchRewards(liked, msg.sender);
}
}

Risk

Likelihood:

  • Users interacting via a frontend that pre-fills 1 ETH will not be affected in normal conditions, but wallet UX errors, gas estimation rounding, and direct contract calls routinely result in slightly over-sent values.

  • Any user calling the contract directly (via Etherscan, cast, scripts) without knowing the exact requirement will overpay and lose the excess permanently.

  • On high-gas networks, some wallets add a small ETH buffer to payable calls automatically.

Impact:

  • Any ETH above 1 ether sent in a likeUser() call is permanently lost with no recovery mechanism for the user.

  • Combined with the missing userBalances credit bug, the entire msg.value (not just the excess) is locked — making this a total loss for every caller.

  • The receive() fallback compounds this: any ETH sent directly to the contract is also unrecoverable.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../src/LikeRegistry.sol";
import "../src/SoulboundProfileNFT.sol";
contract ExcessEthLockedTest is Test {
LikeRegistry registry;
SoulboundProfileNFT nft;
address alice = makeAddr("alice");
address bob = makeAddr("bob");
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", 27, "ipfs://bob");
}
function test_excessEthLocked() public {
deal(alice, 5 ether);
// Alice accidentally sends 2 ETH instead of 1
vm.prank(alice);
registry.likeUser{value: 2 ether}(bob);
// 2 ETH is now in the registry
assertEq(address(registry).balance, 2 ether);
// Alice has no way to get back her extra 1 ETH
// No withdrawUser(), no cancelLike() refund, nothing
assertEq(alice.balance, 3 ether); // lost 2 ETH, not 1
}
}

Recommended Mitigation

Enforce exact payment with == to prevent silent over-collection:

function likeUser(address liked) external payable {
// @> Change >= to == to enforce exact payment
require(msg.value == 1 ether, "Must send exactly 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");
likes[msg.sender][liked] = true;
userBalances[msg.sender] += msg.value;
emit Liked(msg.sender, liked);
if (likes[liked][msg.sender]) {
matches[msg.sender].push(liked);
matches[liked].push(msg.sender);
emit Matched(msg.sender, liked);
matchRewards(liked, msg.sender);
}
}
Updates

Lead Judging Commences

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