DatingDapp

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

Funds Are Permanently Locked Due to Broken Fee, Reward & Withdraw Logic.

Summary

The contract requires users to send ETH (1 ETH per like), but due to a flaw in how fees and balances are managed, this ETH becomes inaccessible.

Vulnerability Details

The fee never increased in the matchRewards function, the same as the user balance, of users were not tracked. This will lead to zero(0) fees at all times.

The withdraw function only withdraws the total fees accumulated, which in this case will be zero and always revert.

function withdrawFees() external onlyOwner {
require(totalFees > 0, "No fees to withdraw"); ///@audit-info No fees to withdraw
@>>> uint256 totalFeesToWithdraw = totalFees;
totalFees = 0;
(bool success, ) = payable(owner()).call{value: totalFeesToWithdraw}(
""
);
require(success, "Transfer failed");
}

Impact

  • Permanent Fund Loss: ETH sent by users remains stuck in the contract forever.

  • Contract Owner Cannot Recover Fees: The business model fails because fees are never collected.

  • Smart Contract Becomes Non-Functional: Users will abandon the contract as there’s no benefit to interacting with it.

POC

function test_WithdrawIsZero_WhenOwner_Withdraw_and_Funds_Still_InContract() public {
vm.prank(user); // Simulates user calling the function
likeRegistry.likeUser{value: 1 ether}(user2); // User 1 likes User 2
assertTrue(likeRegistry.likes(user, user2));
// Simulates user calling the function to trigger the match matchRewards function
vm.prank(user2);
likeRegistry.likeUser{value: 1 ether}(user); // User 1 likes User 2
assertTrue(likeRegistry.likes(user2, user));
uint256 userOneBalance = likeRegistry.userBalances(user);
uint256 userTwoBalance = likeRegistry.userBalances(user2);
assertEq(userOneBalance, 0);
assertEq(userTwoBalance, 0);
console.log("-------- Before Fee Withdraw --------");
console.log("Balance: ", address(likeRegistry).balance / 1e18);
vm.prank(owner);
vm.expectRevert("No fees to withdraw");
likeRegistry.withdrawFees();
console.log("-------- After Fee Withdraw --------");
console.log("Balance: ", address(likeRegistry).balance / 1e18);
}
Ran 1 test for test/testLikeRegistry.t.sol:SoulboundProfileNFTTest
[PASS] test_WithdrawIsZero_WhenOwner_Withdraw_and_Funds_Still_InContract() (gas: 722357)
Logs:
-------- Before Fee Withdraw --------
Balance: 2
-------- After Fee Withdraw --------
Balance: 2
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.16ms (1.96ms CPU time)
Ran 1 test suite in 388.15ms (3.16ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

We can see that the funds are stuck in the contract, and the withdrawFee reverts to No fees to withdraw

Tools Used

Manual Review and Foundry

Recommendations

Modify likeUser to store user deposits correctly

userBalances[msg.sender] += msg.value;
  • This ensures that users' ETH is accounted for when they interact with the contract.

Fix Fee Calculation

  • Ensure that totalFees is correctly calculated:

uint256 matchingFees = (userBalances[from] + userBalances[to]) * FIXEDFEE / 100;
totalFees += matchingFees;

Implement an withdrawal to Withdraw entire contract ETH balance

function withdrawFees() external onlyOwner {
(bool success, ) = payable(owner()).call{value: address(this).balance}("");
require(success, "Withdraw failed");
}
  • This ensures trapped ETH can be withdrawn in case of future failures.

Updates

Appeal created

n0kto Lead Judge 5 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.