DatingDapp

AI First Flight #6
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Severity: high
Valid

LikeRegistry::likeUser accepts 1 ETH but never credits userBalances, causing all match rewards to be zero

Root + Impact

Description

  • The likeUser function requires each caller to send at least 1 ETH (msg.value >= 1 ether). According to the protocol design, these payments should accumulate so that when a mutual match occurs, all previous like payments (minus a 10% fee) are pooled into a shared multisig wallet.

    However, likeUser never updates userBalances[msg.sender]. The ETH is received by the contract but the user's balance mapping remains at zero. When matchRewards is triggered, it reads userBalances[from] and userBalances[to] — both are 0 — so totalRewards is 0, no fees are calculated, and 0 ETH is sent to the newly deployed MultiSig wallet. The 1 ETH per like is permanently trapped in the LikeRegistry contract.

// Root cause in LikeRegistry.sol, likeUser function (lines 31-48)
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;
// @> userBalances[msg.sender] is NEVER updated with msg.value
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);
// @> matchRewards reads userBalances which are 0, sends 0 ETH to MultiSig
}
}

Risk

Likelihood:

  • This occurs on every single call to likeUser. There is no code path that updates userBalances. The bug is triggered 100% of the time.

Impact:

  • All ETH sent to express genuine interest (1 ETH per like) is permanently locked in the LikeRegistry contract with no mechanism for retrieval by users.

  • The core value proposition — pooling match rewards into a shared multisig wallet for the couple's first date — is completely non-functional. The multisig wallets are deployed with zero balance.

  • The only entity that can extract the trapped ETH is the owner via withdrawFees, but totalFees also remains at 0 since it is calculated from the zero totalRewards. The ETH is permanently locked.

Proof of Concept

This test demonstrates that Alice and Bob each send 1 ETH to like each other, triggering a match and deploying a MultiSig wallet. Despite 2 ETH being sent to the contract, both userBalances remain 0, and the MultiSig receives 0 ETH. The 2 ETH is trapped in the LikeRegistry.

function testH01_UserBalancesNeverCredited() public {
// Setup: Alice and Bob mint profiles
vm.prank(alice);
profileNFT.mintProfile("Alice", 25, "ipfs://alice");
vm.prank(bob);
profileNFT.mintProfile("Bob", 27, "ipfs://bob");
// Alice likes Bob with 1 ETH
vm.prank(alice);
likeRegistry.likeUser{value: 1 ether}(bob);
// userBalances is never updated
assertEq(likeRegistry.userBalances(alice), 0); // Should be 1 ether!
// Bob likes Alice with 1 ETH — triggers match
vm.prank(bob);
likeRegistry.likeUser{value: 1 ether}(bob);
// 2 ETH is trapped in LikeRegistry, MultiSig wallet has 0
assertEq(address(likeRegistry).balance, 2 ether);
assertEq(likeRegistry.userBalances(alice), 0);
assertEq(likeRegistry.userBalances(bob), 0);
}

Recommended Mitigation

Add a line in likeUser to credit the sender's balance with the ETH they sent. This ensures matchRewards can correctly pool the accumulated ETH into the MultiSig wallet.

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;
+ userBalances[msg.sender] += msg.value;
emit Liked(msg.sender, liked);
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 3 hours ago
Submission Judgement Published
Validated
Assigned finding tags:

[H-01] After the user calls the `likeUser` function, the userBalance does not increase by the corresponding value.

## Description User A calls `likeUser` and sends `value > 1` ETH. According to the design of DatingDapp, the amount for user A should be accumulated by `userBalances`. Otherwise, in the subsequent calculations, the balance for each user will be 0. ## Vulnerability Details When User A calls `likeUser`, the accumulation of `userBalances` is not performed. ```solidity 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); } } ``` This will result in `totalRewards` always being 0, affecting all subsequent calculations: ```solidity uint256 totalRewards = matchUserOne + matchUserTwo; uint256 matchingFees = (totalRewards * FIXEDFEE ) / 100; uint256 rewards = totalRewards - matchingFees; totalFees += matchingFees; ``` ## POC ```solidity function testUserBalanceshouldIncreaseAfterLike() public { vm.prank(user1); likeRegistry.likeUser{value: 20 ether}(user2); assertEq(likeRegistry.userBalances(user1), 20 ether, "User1 balance should be 20 ether"); } ``` Then we will get an error: ```shell [FAIL: User1 balance should be 20 ether: 0 != 20000000000000000000] ``` ## Impact - Users will be unable to receive rewards. - The contract owner will also be unable to withdraw ETH from the contract. ## Recommendations Add processing for `userBalances` in the `likeUser` function: ```diff 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; + userBalances[msg.sender] += msg.value; emit Liked(msg.sender, liked); [...] } ```

Support

FAQs

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

Give us feedback!