DatingDapp

First Flight #33
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Severity: low
Invalid

Permanent Loss of ETH for Users Without Mutual Like

Summary

In the LikeRegistry contract, users must send at least 1 ETH when liking another user via the likeUser() function. However, if the user never gets a mutual like, the ETH remains locked in the contract indefinitely. There is no mechanism for users to reclaim their ETH if a match never occurs.

Vulnerability Details

When calling LikeRegistry.sol::likeUser(), the sender must send at least 1 ETH, which is intended for distribution upon a successful match. However, the contract only distributes ETH when a mutual like occurs. If the liked user never likes the sender back, the sender's ETH remains trapped in the contract forever. There is no refund mechanism for users who never receive a match which leads to permanent loss of funds.

Impact

  • Users can permanently lose ETH by liking inactive or uninterested users.

  • Attackers could exploit this by creating fake profiles to "trap" ETH from real users.

  • This discourages user engagement, as liking another profile carries the risk of losing funds indefinitely.

  • Over time, the contract could accumulate a significant amount of stranded ETH.

Tools Used

  • Manual review

Recommendations

Implement a refund mechanism and allow users to withdraw their ETH if they have not been matched after a certain period.

function withdrawLike(address liked) external {
require(!likes[liked][msg.sender], "User liked you back");
require(likes[msg.sender][liked], "You did not like this user");
uint256 refundAmount = userBalances[msg.sender];
require(refundAmount > 0, "No balance to withdraw");
userBalances[msg.sender] = 0;
likes[msg.sender][liked] = false;
(bool success, ) = payable(msg.sender).call{value: refundAmount}("");
require(success, "Refund failed");
}

Add the follwoing test to the SoulboundProfileNFTTest.t.sol.

function test_userCanWithdrawFundsIfNotMutualLike() 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);
// a week passed without mutual like, so Alice withdrawals her funds
vm.prank(user);
likeRegistry.withdrawLike(user2);
assertEq(user.balance, 1 ether);
}
Ran 1 test for test/testSoulboundProfileNFT.t.sol:SoulboundProfileNFTTest
[PASS] test_shit() (gas: 400926)
Traces:
[420826] SoulboundProfileNFTTest::test_shit()
├─ [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]
├─ [53299] 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(0x0000000000000000000000000000000000000123)
│ └─ ← [Return]
├─ [33239] LikeRegistry::withdrawLike(0x0000000000000000000000000000000000000456)
│ ├─ [0] 0x0000000000000000000000000000000000000123::fallback{value: 1000000000000000000}()
│ │ └─ ← [Stop]
│ └─ ← [Stop]
├─ [0] VM::assertEq(1000000000000000000 [1e18], 1000000000000000000 [1e18]) [staticcall]
│ └─ ← [Return]
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 822.29µs (231.68µs CPU time)
Updates

Appeal created

n0kto Lead Judge 6 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

invalid_no_withdrawing_function_and_like_all_used

Money collected will be sent to the MultisigWallet during the first match. Emergency withdraw could lead to a frontrun before a match. "If the like is mutual, all their previous like payments (minus a 10% fee) are pooled into a shared multisig wallet" Design choice

0xalipede Submitter
6 months ago
n0kto Lead Judge
6 months ago
n0kto Lead Judge 6 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

invalid_no_withdrawing_function_and_like_all_used

Money collected will be sent to the MultisigWallet during the first match. Emergency withdraw could lead to a frontrun before a match. "If the like is mutual, all their previous like payments (minus a 10% fee) are pooled into a shared multisig wallet" Design choice

Support

FAQs

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