Summary
The likeUser()
function requires users to send 1 ETH when liking another user, but it does not store the funds in userBalances
. As a result, the ETH is permanently lost, preventing users from retrieving their funds. Since matchRewards()
relies on userBalances
to distribute rewards, no ETH is sent to the multisig wallet, effectively burning all like payments.
Vulnerability Details
The contract fails to update userBalances[msg.sender]
when users send ETH:
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);
if (likes[liked][msg.sender]) {
matches[msg.sender].push(liked);
matches[liked].push(msg.sender);
emit Matched(msg.sender, liked);
matchRewards(liked, msg.sender);
}
}
Since matchRewards()
retrieves balances from userBalances
, which remains 0
, no ETH is distributed, and funds are trapped in the contract.
Impact
This issue results in the permanent loss of ETH for users who send payments via likeUser()
. It effectively burns user funds, breaking the expected behavior of the protocol and making it impossible for users to reclaim their ETH in the event of a match.
Key Impacts:
Users send 1 ETH per like, but the contract forgets it.
Matched users receive no ETH in their multisig wallet.
Funds are permanently trapped in the contract, making them unrecoverable.
Proof of Concept (PoC)
The following Foundry test demonstrates how ETH sent to likeUser()
is lost, proving the vulnerability:
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../src/LikeRegistry.sol";
import "../src/SoulboundProfileNFT.sol";
contract LikeRegistryTest is Test {
LikeRegistry likeRegistry;
SoulboundProfileNFT soulboundNFT;
address user1 = address(0x123);
address user2 = address(0x456);
address owner = address(this);
function setUp() public {
soulboundNFT = new SoulboundProfileNFT();
likeRegistry = new LikeRegistry(address(soulboundNFT));
vm.prank(user1);
soulboundNFT.mintProfile("Alice", 25, "ipfs://profileAlice");
vm.prank(user2);
soulboundNFT.mintProfile("Bob", 28, "ipfs://profileBob");
}
function testLostLikePayment() public {
vm.deal(user1, 10 ether);
vm.prank(user1);
likeRegistry.likeUser{value: 1 ether}(user2);
uint256 balanceAfterLike = likeRegistry.userBalances(user1);
assertEq(balanceAfterLike, 0, "Bug not present: ETH was actually stored correctly.");
uint256 contractBalance = address(likeRegistry).balance;
assertGt(contractBalance, 0, "ETH is stuck in the contract");
}
}
Run the test:
forge test --mt testLostLikePayment -vvvv
[⠊] Compiling...
No files changed, compilation skipped
Ran 1 test for test/testLikeRegistry.t.sol:LikeRegistryTest
[PASS] testLostLikePayment() (gas: 61724)
Traces:
[61724] LikeRegistryTest::testLostLikePayment()
├─ [0] VM::deal(0x0000000000000000000000000000000000000123, 10000000000000000000 [1e19])
│ └─ ← [Return]
├─ [0] VM::prank(0x0000000000000000000000000000000000000123)
│ └─ ← [Return]
├─ [37514] LikeRegistry::likeUser{value: 1000000000000000000}(0x0000000000000000000000000000000000000456)
│ ├─ [2627] SoulboundProfileNFT::profileToToken(0x0000000000000000000000000000000000000123) [staticcall]
│ │ └─ ← [Return] 1
│ ├─ [2627] SoulboundProfileNFT::profileToToken(0x0000000000000000000000000000000000000456) [staticcall]
│ │ └─ ← [Return] 2
│ ├─ emit Liked(liker: 0x0000000000000000000000000000000000000123, liked: 0x0000000000000000000000000000000000000456)
│ └─ ← [Stop]
├─ [2561] LikeRegistry::userBalances(0x0000000000000000000000000000000000000123) [staticcall]
│ └─ ← [Return] 0
├─ [0] VM::assertEq(0, 0, "User1's ETH is not stored in userBalances") [staticcall]
│ └─ ← [Return]
├─ [0] VM::assertGt(1000000000000000000 [1e18], 0, "ETH is stuck in the contract") [staticcall]
│ └─ ← [Return]
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.79ms (345.94µs CPU time)
Tools Used
Manual code review
Foundry (Forge)
Recommendations
Modify likeUser()
to correctly store ETH contributions and update userBalances
:
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");
userBalances[msg.sender] += msg.value;
likes[msg.sender][liked] = true;
emit Liked(msg.sender, liked);
if (likes[liked][msg.sender]) {
matches[msg.sender].push(liked);
matches[liked].push(msg.sender);
emit Matched(msg.sender, liked);
matchRewards(liked, msg.sender);
}
}