DatingDapp

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

Incorrect User Balance Handling in matchRewards() Locks Funds Forever

Summary

In the LikeRegistry contract, the matchRewards() function fails to update user balances properly, causing the reward distribution to always fail. The ETH deposited by users in likeUser() is not stored in the userBalances mapping, meaning that when users match, the matchRewards() function will calculate the reward as 0. This issue prevents users from receiving their rewards and their funds are locked forever in the contract.

Vulnerability Details

Affected code

The LikeRegistry.sol::likeUser() function allows users to send ETH when liking another user. However, the userBalances mapping, which should store the amount of ETH a user deposits, is never updated. As a result, when the matchRewards() function is called, the total rewards are always calculated as 0, and no ETH is transferred to the newly deployed MultiSig wallet. This issue effectively breaks the core functionality of the protocol, as users are not receiving the ETH rewards from matching and their funds are locked forever.

PoC

Add the following test suit to the SoulboundProfileNFTTest.t.sol and modify the setUp() .

import "forge-std/Test.sol";
import "../src/SoulboundProfileNFT.sol";
import "../src/LikeRegistry.sol"; // import LikeRegistry
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
contract SoulboundProfileNFTTest is Test {
SoulboundProfileNFT soulboundNFT;
address user = address(0x123);
address user2 = address(0x456);
address owner = address(this); // Test contract acts as the owner
LikeRegistry private likeRegistry;
function setUp() public {
soulboundNFT = new SoulboundProfileNFT();
likeRegistry = new LikeRegistry(address(soulboundNFT)); // deploy LikeRegistry
}
function test_matchRewardsFunctionWillAlwaysSendZeroRewards() public {
// Alice mints profile
vm.prank(user);
vm.deal(user, 1 ether);
soulboundNFT.mintProfile("Alice", 25, "ipfs://profileImage");
// Bob mints profile
vm.prank(user2);
vm.deal(user2, 1 ether);
soulboundNFT.mintProfile("Bob", 25, "ipfs://profileImage");
// Alice likes Bob
vm.prank(user);
likeRegistry.likeUser{value: 1 ether}(user2);
// bob can't believe a girl liked him, so he immediately likes her back
vm.prank(user2);
likeRegistry.likeUser{value: 1 ether}(user);
// Even after a match the ETH was not send to the Multisig but stayed in the contract
assertEq(address(likeRegistry).balance, 2 ether);
}
}
Ran 1 test for test/testSoulboundProfileNFT.t.sol:SoulboundProfileNFTTest
[PASS] test_matchRewardsFunctionWillAlwaysSendZeroRewards() (gas: 1018803)
Traces:
[1018803] SoulboundProfileNFTTest::test_matchRewardsFunctionWillAlwaysSendZeroRewards()
├─ [0] VM::prank(0x0000000000000000000000000000000000000123)
│ └─ ← [Return]
├─ [0] VM::deal(0x0000000000000000000000000000000000000123, 1000000000000000000 [1e18])
│ └─ ← [Return]
├─ [163928] SoulboundProfileNFT::mintProfile("Alice", 25, "ipfs://profileImage")
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0x0000000000000000000000000000000000000123, tokenId: 1)
│ ├─ emit ProfileMinted(user: 0x0000000000000000000000000000000000000123, tokenId: 1, name: "Alice", age: 25, profileImage: "ipfs://profileImage")
│ └─ ← [Stop]
├─ [0] VM::prank(0x0000000000000000000000000000000000000456)
│ └─ ← [Return]
├─ [0] VM::deal(0x0000000000000000000000000000000000000456, 1000000000000000000 [1e18])
│ └─ ← [Return]
├─ [142028] SoulboundProfileNFT::mintProfile("Bob", 25, "ipfs://profileImage")
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0x0000000000000000000000000000000000000456, tokenId: 2)
│ ├─ emit ProfileMinted(user: 0x0000000000000000000000000000000000000456, tokenId: 2, name: "Bob", age: 25, profileImage: "ipfs://profileImage")
│ └─ ← [Stop]
├─ [0] VM::prank(0x0000000000000000000000000000000000000123)
│ └─ ← [Return]
├─ [31029] LikeRegistry::likeUser{value: 1000000000000000000}(0x0000000000000000000000000000000000000456)
│ ├─ [630] SoulboundProfileNFT::profileToToken(0x0000000000000000000000000000000000000123) [staticcall]
│ │ └─ ← [Return] 1
│ ├─ [630] SoulboundProfileNFT::profileToToken(0x0000000000000000000000000000000000000456) [staticcall]
│ │ └─ ← [Return] 2
│ ├─ emit Liked(liker: 0x0000000000000000000000000000000000000123, liked: 0x0000000000000000000000000000000000000456)
│ └─ ← [Stop]
├─ [0] VM::prank(0x0000000000000000000000000000000000000456)
│ └─ ← [Return]
├─ [646796] LikeRegistry::likeUser{value: 1000000000000000000}(0x0000000000000000000000000000000000000123)
│ ├─ [630] SoulboundProfileNFT::profileToToken(0x0000000000000000000000000000000000000456) [staticcall]
│ │ └─ ← [Return] 2
│ ├─ [630] SoulboundProfileNFT::profileToToken(0x0000000000000000000000000000000000000123) [staticcall]
│ │ └─ ← [Return] 1
│ ├─ emit Liked(liker: 0x0000000000000000000000000000000000000456, liked: 0x0000000000000000000000000000000000000123)
│ ├─ emit Matched(user1: 0x0000000000000000000000000000000000000456, user2: 0x0000000000000000000000000000000000000123)
│ ├─ [491245] → new MultiSigWallet@0xffD4505B3452Dc22f8473616d50503bA9E1710Ac
│ │ └─ ← [Return] 2230 bytes of code
│ ├─ [55] MultiSigWallet::receive()
│ │ └─ ← [Stop]
│ └─ ← [Stop]
├─ [0] VM::assertEq(2000000000000000000 [2e18], 2000000000000000000 [2e18]) [staticcall]
│ └─ ← [Return]
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.08ms (355.59µs CPU time)

Impact

  • Users are unable to receive the ETH they deposit when they match with another user.

  • This issue affects the entire reward distribution mechanism and renders the matching functionality useless.

  • The protocol will not be able to fulfill its intended purpose of transferring rewards to users’ MultiSig wallets.

  • Users lose their trust of the protocol.

Tools Used

  • Manual review

  • Foundry

Recommendations

In the likeUser() function, update the userBalances mapping to store the ETH deposited by users.

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");
// update the balance of the depositor
userBalances[msg.sender] += msg.value;
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);
}
}

Updates

Appeal created

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