Description
In LikeRegistry::likeUser
, the userBalances
are not updated to reflect the users msg.value
. According to the protocol docs, "If the like is mutual, all their previous like payments (minus a 10% fee) are pooled into a shared multisig wallet, which both users can access for their first date". This suggests that userBalance
should be updated each time a user pays 1 ETH and "likes" another user.
No where in the code is userBalances
being updated. This has a significant impact on the intended flow and functionality of the protocol.
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);
}
}
Impact
User funds and owner fees will be stuck in the LikeRegistry
contract with no way for the users or the owner to withdraw it.
Proof of Concept
Users create profiles.
Users send 1 ETH and "like" each other - userBalances remain 0.
Calculations are based on 0 amounts in matchRewards
and totalFees remains 0.
No ETH will be transferred to the MultisigWallet
.
LikeRegistry
contract balance is 2 ETH.
Place the following code in a new file testLikeRegistry.t.sol
:
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 profileNFT;
address user = address(0x123);
address user2 = address(0x456);
function setUp() public {
profileNFT = new SoulboundProfileNFT();
likeRegistry = new LikeRegistry(address(profileNFT));
vm.deal(user, 10 ether);
vm.deal(user2, 10 ether);
}
function testUserBalances() public {
vm.prank(user2);
profileNFT.mintProfile("Bob", 30, "ipfs://profileImage");
vm.startPrank(user);
profileNFT.mintProfile("Alice", 25, "ipfs://profileImage");
console.log("Before liking, User1's balance:", likeRegistry.userBalances(user));
console.log("Before liking, User2's balance:", likeRegistry.userBalances(user2));
likeRegistry.likeUser{value: 1 ether}(user2);
console.log("After User1 likes, User1's balance:", likeRegistry.userBalances(user));
console.log("After User1 likes, User2's balance:", likeRegistry.userBalances(user2));
vm.stopPrank();
vm.prank(user2);
likeRegistry.likeUser{value: 1 ether}(user);
console.log("After mutual like, User1's balance:", likeRegistry.userBalances(user));
console.log("After mutual like, User2's balance:", likeRegistry.userBalances(user2));
uint256 totalFees = likeRegistry.getTotalFees();
console.log("Total fees: ", totalFees);
console.log("Contract balance: ", address(likeRegistry).balance);
}
}
Add the following getter function to LikeRegistry.sol
:
function getTotalFees() external view returns (uint256) {
return totalFees;
}
Tools Used
Manual review.
Recommendations
To prevent the funds getting locked in the contract, updating the userBalances
mapping is essential.
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);
+ userBalances[msg.sender] += msg.value;
// Check if mutual like
if (likes[liked][msg.sender]) {
matches[msg.sender].push(liked);
matches[liked].push(msg.sender);
emit Matched(msg.sender, liked);
matchRewards(liked, msg.sender);
}
}