Summary
The LikeRegistry contract incorrectly tracks ETH deposits in the userBalances
mapping, leading to unintended behavior when distributing rewards for mutual likes.
Vulnerability Details
The userBalances
accumulates all ETH sent by a user across multiple likeUser
calls, rather than keeping track of funds on a per-recipient basis. When a mutual like occurs, the entire balance of both users is used to calculate rewards, including funds from unreciprocated likes, which can lead to unfair fund allocation and unintended loss of funds.
Affected Function: matchRewards
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");
}
Explanation of the Issue :
Cumulative Tracking: The userBalances
mapping accumulates ETH sent by a user across all likeUser
calls, but does not differentiate between different recipients.
Incorrect Reward Calculation: When a mutual like occurs, matchRewards
resets the entire balance of both users (userBalances[from] and userBalances[to]), even though some of these funds may have been intended for other users who did not reciprocate the like.
Loss of Funds: A user who has liked multiple people but received only one reciprocated like will have all their ETH used for rewards to the matched pair, potentially leaving other likes underfunded.
Example Scenario:
Alex likes Rose (sends 1 ETH, userBalances[Alex] = 1)
Alex likes Jamie (sends 1 ETH, userBalances[Alex] = 2)
Jamie likes Alex (mutual like triggers matchRewards)
The contract uses userBalances[Alex] = 2 ether and userBalances[Jamie] = 1 for rewards, including the ETH Alex sent for Rose
Result: The 1 ETH Alex intended for Rose is incorrectly allocated to Jamie. Rose will not receive the correct reward if he later reciprocates Alex’s like.
Impact
• Financial Loss: Users may lose funds intended for other potential matches.
• Unfair Reward Distribution: Some users receive more ETH than expected, while others lose their funds.
• Misleading Accounting: Users cannot correctly track or withdraw ETH allocated for specific likes.
POC:
How To Run This PoC:
Step 1: Update the likeUser
function to store the balances of the user by adding the following statement after the checks:
userBalances[msg.sender] += msg.value;
This is also a vulnerability for which I have already submitted a bug report.
Step 2: Add getOwners()
function in the MultiSigWallet
contract:
function getOwners() external view returns(address _owner1, address _owner2) {
_owner1 = owner1;
_owner2 = owner2;
return (_owner1, _owner2);
}
Step 3: Declare the MultiSigWallet
outside the function matchRewards
and make this public, then use it to create new MultiSigWallet
.
MultiSigWallet public multiSigWallet;
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 = new MultiSigWallet(from, to);
(bool success, ) = payable(address(multiSigWallet)).call{value: rewards}("");
require(success, "Transfer failed");
}
Here is the PoC code to demonstrate the issue using foundry:
pragma solidity ^0.8.19;
import { Test, console } from "lib/forge-std/src/Test.sol";
import {Vm} from "lib/forge-std/src/Vm.sol";
import { LikeRegistry } from "../src/LikeRegistry.sol";
import { MultiSigWallet } from "src/MultiSig.sol";
import { SoulboundProfileNFT} from "src/SoulboundProfileNFT.sol";
contract LikeRegistryTest is Test {
LikeRegistry likeRegistry;
MultiSigWallet multiSig;
SoulboundProfileNFT profileNft;
uint256 public constant STARTING_USER_BALANCE = 10 ether;
address public USER1 = makeAddr("user1");
address public USER2 = makeAddr("user2");
address public USER3 = makeAddr("user3");
function setUp() external {
profileNft = new SoulboundProfileNFT();
likeRegistry = new LikeRegistry(address(profileNft));
vm.deal(USER1, STARTING_USER_BALANCE);
vm.deal(USER2, STARTING_USER_BALANCE);
vm.deal(USER3, STARTING_USER_BALANCE);
}
modifier nftMintedForAllTheUsers() {
vm.prank(USER1);
profileNft.mintProfile("Alex", 21, "ipfs://QmUPjADFGEKmfohdTaNcWhp7VGk26h5jXDA7v3VtTnTLcW");
vm.prank(USER2);
profileNft.mintProfile("Rose", 20, "ipfs://RandomSampleURI");
vm.prank(USER3);
profileNft.mintProfile("Jamy", 36, "ipfs://oiasdifulgxzvl0");
_;
}
function test_IncorrectRewardDistributionDueToAggregatedUserBalances() public nftMintedForAllTheUsers{
vm.prank(USER1);
likeRegistry.likeUser{value: 1 ether}(USER2);
vm.prank(USER1);
likeRegistry.likeUser{value: 1 ether}(USER3);
vm.prank(USER3);
likeRegistry.likeUser{value: 1 ether}(USER1);
console.log("Wallet Balance: ", address(likeRegistry.multiSigWallet()).balance);
(address actualOwner3, address actualOwner4) = likeRegistry.multiSigWallet().getOwners();
assertEq(USER1, actualOwner3);
assertEq(USER3, actualOwner4);
}
}
Use this command to run the Poc:
Tools Used
Recommendations
Step 1: Maintain a separate mapping to track ETH set per recipient:
Mapping(address => mapping(address => uint256)) public userSentBalances;
Step 2: Update likeUser
to store ETH sent per liked address:
userSentBalances[msg.sender][liked] += msg.value;
Step 3: Modify matchRewards
to use only the ETH sent specifically for the reciprocated like:
uint256 matchUserOne = userSentBalance[from][to];
uint256 matchUserTwo = userSentBalance[to][from];
userSentBalances[from][to] = 0;
userSentBalances[to][from] = 0;