DatingDapp

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

Zero Balance Calculation in matchRewards Function

Summary

The LikeRegistry contract contains a critical accounting vulnerability where user balances are not tracked when ETH is sent, resulting in zero reward calculations and failed reward distribution to matched users.

Vulnerability Details

The LikeRegistry contract implements a dating platform where users must stake 1 ETH to "like" another user. When two users match, their staked ETH should be combined (minus fees) and sent to a shared MultiSig wallet. However, due to an accounting error, this reward distribution never occurs.

The vulnerability stems from a critical oversight where the contract never updates userBalances when users send ETH through the likeUser function. This causes the matchRewards function to always calculate rewards as 0, regardless of how much ETH users have actually sent.

Vulnerable 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);
// 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");
}

Proof of Concept

Below is a Foundry test demonstrating the vulnerability:

contract LikeRegistryTest is Test {
LikeRegistry public registry;
SoulboundProfileNFT public nft;
address alice = address(0x1);
address bob = address(0x2);
function setUp() public {
// Deploy contracts
nft = new SoulboundProfileNFT();
registry = new LikeRegistry(address(nft));
// Setup user profiles and ETH
vm.deal(alice, 1 ether);
vm.deal(bob, 1 ether);
vm.startPrank(alice);
nft.mintProfile("Alice", 25, "alice.jpg");
vm.stopPrank();
vm.startPrank(bob);
nft.mintProfile("Bob", 28, "bob.jpg");
vm.stopPrank();
}
function testZeroRewardsVulnerability() public {
// Initial contract state
uint256 initialBalance = address(registry).balance;
// Alice likes Bob with 1 ETH
vm.prank(alice);
registry.likeUser{value: 1 ether}(bob);
// Verify ETH was received but not tracked
assertEq(address(registry).balance, initialBalance + 1 ether);
assertEq(registry.userBalances(alice), 0, "User balance should be 0 (not tracked)");
// Bob likes Alice with 1 ETH
vm.startPrank(bob);
registry.likeUser{value: 1 ether}(alice);
// Get created MultiSig address
address[] memory aliceMatches = registry.getMatches();
require(aliceMatches.length > 0, "Match not created");
address multiSigAddress = aliceMatches[0];
bytes32 totalFeesSlot = keccak256("totalFees");
uint256 storedTotalFees = uint256(vm.load(address(registry), totalFeesSlot));
vm.stopPrank();
// Verify the vulnerability
assertEq(address(registry).balance, initialBalance + 2 ether, "Contract should hold 2 ETH");
assertEq(address(multiSigAddress).balance, 0, "MultiSig received no ETH due to vulnerability");
assertEq(storedTotalFees, 0, "No fees were collected");
}
}

Root Cause

The vulnerability exists in two parts:

  • Missing balance tracking in likeUser:

function likeUser(address liked) external payable {
require(msg.value >= 1 ether, "Must send at least 1 ETH");
// msg.value is never added to userBalances
likes[msg.sender][liked] = true;
}
  • Zero reward calculation in matchRewards:

function matchRewards(address from, address to) internal {
uint256 matchUserOne = userBalances[from]; // Always 0
uint256 matchUserTwo = userBalances[to]; // Always 0
uint256 totalRewards = matchUserOne + matchUserTwo; // Always 0
uint256 matchingFees = (totalRewards * FIXEDFEE) / 100; // Always 0
uint256 rewards = totalRewards - matchingFees; // Always 0
// Creates MultiSig with 0 balance
MultiSigWallet multiSigWallet = new MultiSigWallet(from, to);
(bool success,) = payable(address(multiSigWallet)).call{value: rewards}("");
}

Severity

Critical - Direct loss of user funds

Impact

  • All matched users receive 0 ETH in their MultiSig wallet

  • Platform fees are incorrectly calculated as 0

  • User funds become permanently trapped in the contract

  • Core matching reward functionality is completely broken

  • Loss of protocol revenue through missed fees

Tools Used

Manual Review

Recommendations

  • Implement proper balance tracking in likeUser:

function likeUser(address liked) external payable {
require(msg.value >= 1 ether, "Must send at least 1 ETH");
userBalances[msg.sender] += msg.value; // Track user's ETH
likes[msg.sender][liked] = true;
}
  • Add global balance tracking:

uint256 public totalDeposits;
function likeUser(address liked) external payable {
require(msg.value >= 1 ether, "Must send at least 1 ETH");
userBalances[msg.sender] += msg.value;
totalDeposits += msg.value;
}
  • Add balance validation in matchRewards:

function matchRewards(address from, address to) internal {
uint256 matchUserOne = userBalances[from];
uint256 matchUserTwo = userBalances[to];
require(matchUserOne > 0 && matchUserTwo > 0, "Invalid balances");
require(
address(this).balance >= matchUserOne + matchUserTwo,
"Insufficient contract balance"
);
// Rest of the function...
}
  • Consider implementing events for balance tracking:

event BalanceUpdated(address user, uint256 newBalance);
event RewardsDistributed(address from, address to, uint256 amount, uint256 fees);
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.