DatingDapp

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

Loss of Funds Due to Missing Balance Updates in LikeRegistry

Summary

The LikeRegistry contract contains a critical flaw in the likeUser function, where ETH sent by users is not recorded in userBalances. This results in users being unable to retrieve or properly utilize their deposited funds, effectively leading to the loss of ETH. The intended design seems to require these funds to be pooled and later distributed upon a match, but due to the missing balance updates, all ETH remains stuck in the contract.

Vulnerability Details

The root cause of this issue lies in how the likeUser function is implemented. Below is a relevant code snippet from the contract:

function likeUser(address liked) external payable {
require(msg.value >= 1 ether, "Minimum 1 ETH required");
uint256 likerTokenId = soulboundNFT.profileToToken(msg.sender);
uint256 likedTokenId = soulboundNFT.profileToToken(liked);
emit Liked(msg.sender, liked);
if (likes[likedTokenId] == likerTokenId) {
emit Matched(msg.sender, liked);
new MultiSigWallet();
}
}

In this implementation, while the function ensures that at least 1 ETH is sent, it fails to update userBalances. The missing logic should have been something like:

userBalances[msg.sender] += msg.value;

Without this update, the ETH is received by the contract but never associated with the sender’s balance. As a result:

  • Users effectively lose the ETH they send, as they have no way to retrieve it.

  • The logic meant to accumulate and later distribute funds upon a match does not function correctly.

  • Even when a match occurs, 0 ETH is transferred to the MultiSigWallet, making the process ineffective.

proof of concept

create a new test file and use this

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../src/SoulboundProfileNFT.sol";
import "../src/LikeRegistry.sol";
contract LikeRegistryPoCTest is Test {
SoulboundProfileNFT profileNFT;
LikeRegistry likeRegistry;
// Define three test addresses
address user1 = address(0x100);
address user2 = address(0x200);
address owner = address(this); // The test contract acts as the owner
function setUp() public {
// Deploy the SoulboundProfileNFT and LikeRegistry contracts.
profileNFT = new SoulboundProfileNFT();
likeRegistry = new LikeRegistry(address(profileNFT));
// Mint profiles for user1 and user2.
vm.prank(user1);
profileNFT.mintProfile("Alice", 25, "ipfs://profileAlice");
vm.prank(user2);
profileNFT.mintProfile("Bob", 30, "ipfs://profileBob");
}
function testLackOfBalanceUpdates() public {
// Fund user1 and user2 with ETH before interaction
vm.deal(user1, 10 ether);
vm.deal(user2, 10 ether);
// User1 likes User2
vm.prank(user1);
likeRegistry.likeUser{value: 1 ether}(user2);
// User2 likes User1 (expecting a mutual match)
vm.prank(user2);
likeRegistry.likeUser{value: 1 ether}(user1);
// Check contract balance
assertEq(address(likeRegistry).balance, 2 ether, "Funds should be stuck in LikeRegistry");
assertEq(likeRegistry.userBalances(user1), 0, "User1 balance should NOT be updated");
assertEq(likeRegistry.userBalances(user2), 0, "User2 balance should NOT be updated")
}
}

log of the poc

Ran 1 test for test/LikeRegistryPoCTest.t.sol:LikeRegistryPoCTest
[PASS] testLackOfBalanceUpdates() (gas: 710189)
Traces:
[710189] LikeRegistryPoCTest::testLackOfBalanceUpdates()
├─ [0] VM::deal(0x0000000000000000000000000000000000000100, 10000000000000000000 [1e19])
│ └─ ← [Return]
├─ [0] VM::deal(0x0000000000000000000000000000000000000200, 10000000000000000000 [1e19])
│ └─ ← [Return]
├─ [0] VM::prank(0x0000000000000000000000000000000000000100)
│ └─ ← [Return]
├─ [37514] LikeRegistry::likeUser{value: 1000000000000000000}(0x0000000000000000000000000000000000000200)
│ ├─ [2627] SoulboundProfileNFT::profileToToken(0x0000000000000000000000000000000000000100) [staticcall]
│ │ └─ ← [Return] 1
│ ├─ [2627] SoulboundProfileNFT::profileToToken(0x0000000000000000000000000000000000000200) [staticcall]
│ │ └─ ← [Return] 2
│ ├─ emit Liked(liker: 0x0000000000000000000000000000000000000100, liked: 0x0000000000000000000000000000000000000200)
│ └─ ← [Stop]
├─ [0] VM::prank(0x0000000000000000000000000000000000000200)
│ └─ ← [Return]
├─ [639512] LikeRegistry::likeUser{value: 1000000000000000000}(0x0000000000000000000000000000000000000100)
│ ├─ [627] SoulboundProfileNFT::profileToToken(0x0000000000000000000000000000000000000200) [staticcall]
│ │ └─ ← [Return] 2
│ ├─ [627] SoulboundProfileNFT::profileToToken(0x0000000000000000000000000000000000000100) [staticcall]
│ │ └─ ← [Return] 1
│ ├─ emit Liked(liker: 0x0000000000000000000000000000000000000200, liked: 0x0000000000000000000000000000000000000100)
│ ├─ emit Matched(user1: 0x0000000000000000000000000000000000000200, user2: 0x0000000000000000000000000000000000000100)
│ ├─ [483834] → new MultiSigWallet@0xffD4505B3452Dc22f8473616d50503bA9E1710Ac
│ │ └─ ← [Return] 2193 bytes of code
│ ├─ [55] MultiSigWallet::receive()
│ │ └─ ← [Stop]
│ └─ ← [Stop]
├─ [0] VM::assertEq(2000000000000000000 [2e18], 2000000000000000000 [2e18], "Funds should be stuck in LikeRegistry") [staticcall]
│ └─ ← [Return]
├─ [561] LikeRegistry::userBalances(0x0000000000000000000000000000000000000100) [staticcall]
│ └─ ← [Return] 0
├─ [0] VM::assertEq(0, 0, "User1 balance should NOT be updated") [staticcall]
│ └─ ← [Return]
├─ [561] LikeRegistry::userBalances(0x0000000000000000000000000000000000000200) [staticcall]
│ └─ ← [Return] 0
├─ [0] VM::assertEq(0, 0, "User2 balance should NOT be updated") [staticcall]
│ └─ ← [Return]
├─ [0] VM::assertEq(2000000000000000000 [2e18], 2000000000000000000 [2e18], "ETH should remain in contract") [staticcall]
│ └─ ← [Return]
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.47ms (362.60µs CPU time)

The test confirms that funds remain in the LikeRegistry contract and users' balances are not updated

Impact

The impact of this issue is severe, as it directly results in the unintended loss of user funds. Users who attempt to like others by sending ETH will find their funds stuck in the contract, and even if a match occurs, the expected pooling and distribution of ETH will not work. This breaks the core functionality of the protocol and undermines user trust.

Tools used

manual review

Recommendation

To fix this issue, the contract should explicitly update userBalances whenever ETH is received. Modify likeUser as follows:

function likeUser(address liked) external payable {
require(msg.value >= 1 ether, "Minimum 1 ETH required");
userBalances[msg.sender] += msg.value; // Ensure ETH is correctly tracked
uint256 likerTokenId = soulboundNFT.profileToToken(msg.sender);
uint256 likedTokenId = soulboundNFT.profileToToken(liked);
emit Liked(msg.sender, liked);
if (likes[likedTokenId] == likerTokenId) {
emit Matched(msg.sender, liked);
new MultiSigWallet();
}
}
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.