DatingDapp

First Flight #33
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Severity: low
Invalid

Excess ETH gets trapped in LikeRegistry.sol due to missing balance tracking

Summary

The LikeRegistry contract's likeUser() function accepts arbitrary ETH amounts but fails to properly track them in userBalances, leading to trapped ETH when users send more than the required 1 ETH minimum.

Vulnerability Details

When a user calls likeUser() with more than 1 ETH, the excess amount is not tracked in userBalances. This leads to funds being trapped in the contract when matches occur, as matchRewards() only processes the minimum required amount.

function likeUser(address liked) external payable {
require(msg.value >= 1 ether, "Must send at least 1 ETH");
...
}

PoC:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "./SoulboundProfileNFT.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "./MultiSig.sol";
contract LikeRegistry is Ownable{
struct Like {
address liker;
address liked;
uint256 timestamp;
}
SoulboundProfileNFT public profileNFT;
uint256 immutable FIXEDFEE = 10;
uint256 totalFees;
mapping(address => mapping(address => bool)) public likes;
mapping(address => address[]) public matches;
mapping(address => uint256) public userBalances;
event Liked(address indexed liker, address indexed liked);
event Matched(address indexed user1, address indexed user2);
constructor(address _profileNFT) Ownable(msg.sender){
profileNFT = SoulboundProfileNFT(_profileNFT);
}
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);
// Check if mutual like
if (likes[liked][msg.sender]) {
matches[msg.sender].push(liked);
matches[liked].push(msg.sender);
emit Matched(msg.sender, liked);
matchRewards(liked, msg.sender);
}
}
function matchRewards(address from, address to) internal {
uint256 matchUserOne = userBalances[from];
uint256 matchUserTwo = userBalances[to];
userBalances[from] = 0;
userBalances[to] = 0;
uint256 totalRewards = matchUserOne + matchUserTwo;
uint256 matchingFees = (totalRewards * FIXEDFEE ) / 100;
uint256 rewards = totalRewards - matchingFees;
totalFees += matchingFees;
// Deploy a MultiSig contract for the matched users
MultiSigWallet multiSigWallet = new MultiSigWallet(from, to);
// Send ETH to the deployed multisig wallet
(bool success, ) = payable(address(multiSigWallet)).call{value: rewards}("");
require(success, "Transfer failed");
}
function getMatches() external view returns (address[] memory) {
return matches[msg.sender];
}
function withdrawFees() external onlyOwner {
require(totalFees > 0, "No fees to withdraw");
uint256 totalFeesToWithdraw = totalFees;
totalFees = 0;
(bool success, ) = payable(owner()).call{value: totalFeesToWithdraw}("");
require(success, "Transfer failed");
}
/// @notice Allows the contract to receive ETH
receive() external payable {}
}

PoC Result:

forge test --match-contract LikeRegistryBalanceTrackingTest -vvv
[⠆] Compiling...
[⠘] Compiling 1 files with Solc 0.8.28
[⠊] Solc 0.8.28 finished in 385.11ms
Ran 1 test for test/LikeRegistryBalanceTrackingTest.t.sol:LikeRegistryBalanceTrackingTest
[PASS] testExcessEthLoss() (gas: 1103176)
Logs:
Total Protocol Balance: 6000000000000000000
Expected Protocol Fees: 600000000000000000
Extra ETH Trapped: 5400000000000000000
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 979.22µs (149.53µs CPU time)
Ran 1 test suite in 4.54ms (979.22µs CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Impact

Direct loss of user funds

When users send excess ETH (>1 ETH), the extra amount becomes permanently trapped

The trapped ETH cannot be recovered by either users or protocol

Example: Sending 5 ETH instead of 1 ETH leads to 4 ETH being trapped

Tools Used

Manuel code review

Foundry

Recommendations

Track full payment amount:

function likeUser(address liked) external payable {
require(msg.value >= 1 ether, "Must send at least 1 ETH");
userBalances[msg.sender] += msg.value; // Track full amount
...
}

Or enforce exact payment:

function likeUser(address liked) external payable {
require(msg.value == 1 ether, "Must send exactly 1 ETH");
...
}
Updates

Appeal created

n0kto Lead Judge 6 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

Users mistake, only impacting themselves.

Please read the CodeHawks documentation to know which submissions are valid. If you disagree, provide a coded PoC and explain the real likelihood and the detailed impact on the mainnet without any supposition (if, it could, etc) to prove your point.

Support

FAQs

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