DatingDapp

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

likeUser never credits userBalances, so matched couples get 0 ETH and all like payments are locked forever

Root + Impact

Description

  • Users pay at least 1 ETH per like. On a mutual like, matchRewards should pool both users' previous like payments (minus a 10% fee) into a new MultiSig wallet for the pair, and the 10% should become withdrawable protocol fees.

  • likeUser never adds msg.value to userBalances[msg.sender]. userBalances is only ever read and zeroed, so it is always 0. On every match totalRewards = 0, the MultiSig receives 0 ETH and totalFees stays 0. All like payments remain in LikeRegistry, and the only outgoing path, withdrawFees, can only send totalFees (0) and reverts with No fees to withdraw.

function likeUser(address liked) external payable {
require(msg.value >= 1 ether, "Must send at least 1 ETH");
...
likes[msg.sender][liked] = true;
@> // msg.value is never credited to userBalances[msg.sender]
if (likes[liked][msg.sender]) {
...
matchRewards(liked, msg.sender);
}
}
​
function matchRewards(address from, address to) internal {
@> uint256 matchUserOne = userBalances[from]; // always 0
@> uint256 matchUserTwo = userBalances[to]; // always 0
...
@> (bool success,) = payable(address(multiSigWallet)).call{value: rewards}(""); // rewards == 0
}

Path:

  1. Alice likes Bob with 1 ETH; Bob likes Alice with 1 ETH -> match.

  2. matchRewards reads both balances as 0, deploys a MultiSig and sends it 0 ETH; totalFees += 0.

  3. LikeRegistry now holds 2 ETH that no function can move: there is no user withdrawal, and withdrawFees only sends totalFees (0).

Risk

Likelihood:

  • Happens on every like and every match, for every user. No special conditions.

Impact:

  • 100% of user payments are permanently locked in LikeRegistry. Matched couples receive nothing in their shared wallet (the core feature), and the protocol earns no fees.

Proof of Concept

Test setup: owner deploys SoulboundProfileNFT and LikeRegistry(address(nft)); alice, bob, carol each get 10 ETH and mint a profile (token ids 1, 2, 3). Full file test/AuditPoC.t.sol, forge test --mc AuditPoC passes.

function test_F1_likePaymentsNeverReachMultisig() public {
address predicted = _wallet(address(reg), vm.getNonce(address(reg)));
vm.prank(alice);
reg.likeUser{value: 1 ether}(bob);
vm.prank(bob);
reg.likeUser{value: 1 ether}(alice); // mutual -> match, deploys multisig
assertEq(reg.userBalances(alice), 0);
assertEq(reg.userBalances(bob), 0);
assertGt(predicted.code.length, 0); // multisig was deployed
assertEq(predicted.balance, 0); // ...but received nothing (expected 1.8 ETH)
assertEq(address(reg).balance, 2 ether);
vm.prank(owner);
vm.expectRevert("No fees to withdraw");
reg.withdrawFees(); // no path at all to the 2 ETH
}

Recommended Mitigation

Credit the payment when the like is made:

likes[msg.sender][liked] = true;
+ userBalances[msg.sender] += msg.value;
emit Liked(msg.sender, liked);

Add a test that asserts the MultiSig balance equals 90% of both payments after a match.

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 8 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!