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
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;
address user1 = address(0x100);
address user2 = address(0x200);
address owner = address(this);
function setUp() public {
profileNFT = new SoulboundProfileNFT();
likeRegistry = new LikeRegistry(address(profileNFT));
vm.prank(user1);
profileNFT.mintProfile("Alice", 25, "ipfs://profileAlice");
vm.prank(user2);
profileNFT.mintProfile("Bob", 30, "ipfs://profileBob");
}
function testLackOfBalanceUpdates() public {
vm.deal(user1, 10 ether);
vm.deal(user2, 10 ether);
vm.prank(user1);
likeRegistry.likeUser{value: 1 ether}(user2);
vm.prank(user2);
likeRegistry.likeUser{value: 1 ether}(user1);
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;
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();
}
}