DatingDapp

First Flight #33
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Severity: high
Valid

Missing Like Payment Storage Causes Permanent Fund Loss

Summary

The likeUser() function requires users to send 1 ETH when liking another user, but it does not store the funds in userBalances. As a result, the ETH is permanently lost, preventing users from retrieving their funds. Since matchRewards() relies on userBalances to distribute rewards, no ETH is sent to the multisig wallet, effectively burning all like payments.

Vulnerability Details

The contract fails to update userBalances[msg.sender] when users send ETH:

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");
likes[msg.sender][liked] = true;
emit Liked(msg.sender, liked);
// ❌ ETH is not tracked in userBalances
if (likes[liked][msg.sender]) {
matches[msg.sender].push(liked);
matches[liked].push(msg.sender);
emit Matched(msg.sender, liked);
matchRewards(liked, msg.sender); // Sends 0 ETH since userBalances is empty
}
}

Since matchRewards() retrieves balances from userBalances, which remains 0, no ETH is distributed, and funds are trapped in the contract.

Impact

This issue results in the permanent loss of ETH for users who send payments via likeUser(). It effectively burns user funds, breaking the expected behavior of the protocol and making it impossible for users to reclaim their ETH in the event of a match.

Key Impacts:

  • Users send 1 ETH per like, but the contract forgets it.

  • Matched users receive no ETH in their multisig wallet.

  • Funds are permanently trapped in the contract, making them unrecoverable.

Proof of Concept (PoC)

The following Foundry test demonstrates how ETH sent to likeUser() is lost, proving the vulnerability:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../src/LikeRegistry.sol";
import "../src/SoulboundProfileNFT.sol";
contract LikeRegistryTest is Test {
LikeRegistry likeRegistry;
SoulboundProfileNFT soulboundNFT;
address user1 = address(0x123);
address user2 = address(0x456);
address owner = address(this);
function setUp() public {
soulboundNFT = new SoulboundProfileNFT();
likeRegistry = new LikeRegistry(address(soulboundNFT));
// Mint profiles for both users
vm.prank(user1);
soulboundNFT.mintProfile("Alice", 25, "ipfs://profileAlice");
vm.prank(user2);
soulboundNFT.mintProfile("Bob", 28, "ipfs://profileBob");
}
function testLostLikePayment() public {
// User1 likes User2, sending 1 ETH
vm.deal(user1, 10 ether);
vm.prank(user1);
likeRegistry.likeUser{value: 1 ether}(user2);
// Check userBalances (should be 1 ETH if properly stored)
uint256 balanceAfterLike = likeRegistry.userBalances(user1);
assertEq(balanceAfterLike, 0, "Bug not present: ETH was actually stored correctly."); // Expected failure
// Check contract balance (ETH is trapped inside)
uint256 contractBalance = address(likeRegistry).balance;
assertGt(contractBalance, 0, "ETH is stuck in the contract");
}
}

Run the test:

forge test --mt testLostLikePayment -vvvv
[⠊] Compiling...
No files changed, compilation skipped
Ran 1 test for test/testLikeRegistry.t.sol:LikeRegistryTest
[PASS] testLostLikePayment() (gas: 61724)
Traces:
[61724] LikeRegistryTest::testLostLikePayment()
├─ [0] VM::deal(0x0000000000000000000000000000000000000123, 10000000000000000000 [1e19])
│ └─ ← [Return]
├─ [0] VM::prank(0x0000000000000000000000000000000000000123)
│ └─ ← [Return]
├─ [37514] LikeRegistry::likeUser{value: 1000000000000000000}(0x0000000000000000000000000000000000000456)
│ ├─ [2627] SoulboundProfileNFT::profileToToken(0x0000000000000000000000000000000000000123) [staticcall]
│ │ └─ ← [Return] 1
│ ├─ [2627] SoulboundProfileNFT::profileToToken(0x0000000000000000000000000000000000000456) [staticcall]
│ │ └─ ← [Return] 2
│ ├─ emit Liked(liker: 0x0000000000000000000000000000000000000123, liked: 0x0000000000000000000000000000000000000456)
│ └─ ← [Stop]
├─ [2561] LikeRegistry::userBalances(0x0000000000000000000000000000000000000123) [staticcall]
│ └─ ← [Return] 0
├─ [0] VM::assertEq(0, 0, "User1's ETH is not stored in userBalances") [staticcall]
│ └─ ← [Return]
├─ [0] VM::assertGt(1000000000000000000 [1e18], 0, "ETH is stuck in the contract") [staticcall]
│ └─ ← [Return]
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.79ms (345.94µs CPU time)

Tools Used

  • Manual code review

  • Foundry (Forge)

Recommendations

Modify likeUser() to correctly store ETH contributions and update userBalances:

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");
// ✅ FIX: Store ETH in userBalances
userBalances[msg.sender] += msg.value;
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);
}
}
Updates

Appeal created

n0kto Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

finding_likeUser_no_userBalances_updated

Likelihood: High, always. Impact: High, loss of funds

Support

FAQs

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