DatingDapp

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

userBalances are not tracked properly, funds get locked in the contract

Summary

Funds will get locked in the contract because user balances are not properly updated.

Vulnerability Details

In the LikeRegistry::likeUser function, msg.value is never added to the userBalances. The sent ETH will go into the contract balance but as it is not recorded in userBalances, this will later cause LikeRegistry::matchRewards to send effectively 0 to the multi sig wallet and the totalFees will also remain 0:

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;

Proof of Concept

Here is a test that presents a case where both Alice and Bob create profiles, like each other and get matched per mutual like. However, while the contract balance is now 2 ether, the totalFees is not 0.2 ETH but still 0.

// 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 profileNFT;
address alice = address(0x1);
address bob = address(0x2);
// Add receive function to allow test contract to receive ETH
receive() external payable {}
function setUp() public {
// Deploy contracts
profileNFT = new SoulboundProfileNFT();
likeRegistry = new LikeRegistry(address(profileNFT));
// Fund test users
vm.deal(alice, 2 ether);
vm.deal(bob, 2 ether);
// Create profiles for Alice and Bob
vm.prank(alice);
profileNFT.mintProfile("Alice", 25, "ipfs://alice");
vm.stopPrank();
vm.prank(bob);
profileNFT.mintProfile("Bob", 28, "ipfs://bob");
vm.stopPrank();
}
function testMatchRewardWithoutBalanceUpdate() public {
// Alice likes Bob
vm.prank(alice);
likeRegistry.likeUser{value: 1 ether}(bob);
vm.stopPrank();
// Verify the like was recorded
assertTrue(likeRegistry.likes(alice, bob), "Alice's like should be recorded");
// Verify Alice's balance is still 0 even after sending 1 ETH
assertEq(likeRegistry.userBalances(alice), 0, "Alice's balance should be 0 as it's not updated");
// Bob likes Alice back
vm.prank(bob);
likeRegistry.likeUser{value: 1 ether}(alice);
vm.stopPrank();
// Verify Bob's like was recorded
assertTrue(likeRegistry.likes(bob, alice), "Bob's like should be recorded");
// Verify Bob's balance is still 0 even after sending 1 ETH
assertEq(likeRegistry.userBalances(bob), 0, "Bob's balance should be 0 as it's not updated");
// Verify the match happened
vm.prank(alice);
address[] memory aliceMatches = likeRegistry.getMatches();
assertEq(aliceMatches.length, 1, "Should have one match now");
assertEq(aliceMatches[0], bob, "Should be matched with Bob");
vm.stopPrank();
// After matching, total fees should be 0 because userBalances were never updated
assertEq(likeRegistry.totalFees(), 0, "Total fees should be 0 since balances were not tracked properly");
// Verify that contract holds 2 ETH (1 ETH from each like)
assertEq(address(likeRegistry).balance, 2 ether, "Contract should hold 2 ETH from likes");
// Try to withdraw fees as owner - should revert because totalFees is 0
vm.prank(address(this)); // Test contract is the owner
vm.expectRevert("No fees to withdraw");
likeRegistry.withdrawFees();
vm.stopPrank();
}
}

Important note for this test: to be able to check likeRegistry.totalFees() make sure to modify it to be public in LikeRegistry.sol first:

// existing code
uint256 public totalFees;
// existing code

Impact

This will cause funds to get locked in the contract and the couple will be left with no funds in their multi sig wallet on their date.

Tools Used

Manual inspection, copilot and foundry.

Recommended Mitigation

The simple solution is to properly update userBlances in likeUser function:

// existing code
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);
// *************** FIX:
// additional line for properly updating the userBlances with msg.value
userBalances[msg.sender] += msg.value;
// ***************
// existing code
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.