Summary
The LikeRegistry contract contains a critical accounting vulnerability where user balances are not tracked when ETH is sent, resulting in zero reward calculations and failed reward distribution to matched users.
Vulnerability Details
The LikeRegistry contract implements a dating platform where users must stake 1 ETH to "like" another user. When two users match, their staked ETH should be combined (minus fees) and sent to a shared MultiSig wallet. However, due to an accounting error, this reward distribution never occurs.
The vulnerability stems from a critical oversight where the contract never updates userBalances
when users send ETH through the likeUser
function. This causes the matchRewards
function to always calculate rewards as 0, regardless of how much ETH users have actually sent.
Vulnerable Code
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);
}
}
function matchRewards(address from, address to) internal {
uint256 matchUserOne = userBalances[from];
uint256 matchUserTwo = userBalances[to];
userBalances[from] = 0;
userBalances[to] = 0;
uint256 totalRewards = matchUserOne + matchUserTwo;
uint256 matchingFees = (totalRewards * FIXEDFEE) / 100;
uint256 rewards = totalRewards - matchingFees;
totalFees += matchingFees;
MultiSigWallet multiSigWallet = new MultiSigWallet(from, to);
(bool success,) = payable(address(multiSigWallet)).call{value: rewards}("");
require(success, "Transfer failed");
}
Proof of Concept
Below is a Foundry test demonstrating the vulnerability:
contract LikeRegistryTest is Test {
LikeRegistry public registry;
SoulboundProfileNFT public nft;
address alice = address(0x1);
address bob = address(0x2);
function setUp() public {
nft = new SoulboundProfileNFT();
registry = new LikeRegistry(address(nft));
vm.deal(alice, 1 ether);
vm.deal(bob, 1 ether);
vm.startPrank(alice);
nft.mintProfile("Alice", 25, "alice.jpg");
vm.stopPrank();
vm.startPrank(bob);
nft.mintProfile("Bob", 28, "bob.jpg");
vm.stopPrank();
}
function testZeroRewardsVulnerability() public {
uint256 initialBalance = address(registry).balance;
vm.prank(alice);
registry.likeUser{value: 1 ether}(bob);
assertEq(address(registry).balance, initialBalance + 1 ether);
assertEq(registry.userBalances(alice), 0, "User balance should be 0 (not tracked)");
vm.startPrank(bob);
registry.likeUser{value: 1 ether}(alice);
address[] memory aliceMatches = registry.getMatches();
require(aliceMatches.length > 0, "Match not created");
address multiSigAddress = aliceMatches[0];
bytes32 totalFeesSlot = keccak256("totalFees");
uint256 storedTotalFees = uint256(vm.load(address(registry), totalFeesSlot));
vm.stopPrank();
assertEq(address(registry).balance, initialBalance + 2 ether, "Contract should hold 2 ETH");
assertEq(address(multiSigAddress).balance, 0, "MultiSig received no ETH due to vulnerability");
assertEq(storedTotalFees, 0, "No fees were collected");
}
}
Root Cause
The vulnerability exists in two parts:
function likeUser(address liked) external payable {
require(msg.value >= 1 ether, "Must send at least 1 ETH");
likes[msg.sender][liked] = true;
}
function matchRewards(address from, address to) internal {
uint256 matchUserOne = userBalances[from];
uint256 matchUserTwo = userBalances[to];
uint256 totalRewards = matchUserOne + matchUserTwo;
uint256 matchingFees = (totalRewards * FIXEDFEE) / 100;
uint256 rewards = totalRewards - matchingFees;
MultiSigWallet multiSigWallet = new MultiSigWallet(from, to);
(bool success,) = payable(address(multiSigWallet)).call{value: rewards}("");
}
Severity
Critical - Direct loss of user funds
Impact
All matched users receive 0 ETH in their MultiSig wallet
Platform fees are incorrectly calculated as 0
User funds become permanently trapped in the contract
Core matching reward functionality is completely broken
Loss of protocol revenue through missed fees
Tools Used
Manual Review
Recommendations
function likeUser(address liked) external payable {
require(msg.value >= 1 ether, "Must send at least 1 ETH");
userBalances[msg.sender] += msg.value;
likes[msg.sender][liked] = true;
}
uint256 public totalDeposits;
function likeUser(address liked) external payable {
require(msg.value >= 1 ether, "Must send at least 1 ETH");
userBalances[msg.sender] += msg.value;
totalDeposits += msg.value;
}
function matchRewards(address from, address to) internal {
uint256 matchUserOne = userBalances[from];
uint256 matchUserTwo = userBalances[to];
require(matchUserOne > 0 && matchUserTwo > 0, "Invalid balances");
require(
address(this).balance >= matchUserOne + matchUserTwo,
"Insufficient contract balance"
);
}
event BalanceUpdated(address user, uint256 newBalance);
event RewardsDistributed(address from, address to, uint256 amount, uint256 fees);