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
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 {
vm.prank(user);
vm.deal(user, 1 ether);
soulboundNFT.mintProfile("Alice", 25, "ipfs://profileImage");
vm.prank(user2);
vm.deal(user2, 1 ether);
soulboundNFT.mintProfile("Bob", 25, "ipfs://profileImage");
vm.prank(user);
likeRegistry.likeUser{value: 1 ether}(user2);
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)