Summary
Due to fund mishandling in the LikeRegistry::matchRewards
, any deployed MultisigWallet will contain 0 fund, and also fees for the protocol is always 0 as well. User funds sent through LikeRegistry::likeUser
is never being accounted to the user. It leads to the created multisigWallet has no fund in it, and user lose their fund.
Vulnerability Detail
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");
}
As we can see at the above snippet, matchUserOne
and matchUserTwo
are the main variables for this calculation. Its retrieved from the userBalances
variable. But userBalances
variable value actually never increased. As we can see in the below LikeRegistry::likeUser
code:
function likeUser(address liked) external payable {
[...]
likes[msg.sender][liked] = true;
emit Liked(msg.sender, liked);
userBalances[msg.sender] += msg.value;
if (likes[liked][msg.sender]) {
matches[msg.sender].push(liked);
matches[liked].push(msg.sender);
emit Matched(msg.sender, liked);
address multiSigWallet = matchRewards(liked, msg.sender);
}
}
The LikeRegistry::likeUser
require user to send fund greater than or equal to 1 ether, but these 1 ether never being accounted to the sender address.
POC
We modify the src/LikeRegistry.sol
file a bit to return the multisigWallet address from likeUser
and matchRewards
.
LikeRegistry::likeUser
- function likeUser(address liked) external payable {
+ function likeUser(address liked) external payable returns (address) {
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");
@@ -38,16 +38,23 @@ contract LikeRegistry is Ownable {
likes[msg.sender][liked] = true;
emit Liked(msg.sender, liked);
// 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);
+ address multiSigWallet = matchRewards(liked, msg.sender);
+ return multiSigWallet;
}
+
+ return address(0);
}
LikeRegistry::matchRewards
- function matchRewards(address from, address to) internal {
+ function matchRewards(address from, address to) internal returns (address) {
uint256 matchUserOne = userBalances[from];
uint256 matchUserTwo = userBalances[to];
userBalances[from] = 0;
@@ -64,6 +71,8 @@ contract LikeRegistry is Ownable {
(bool success,) = payable(address(multiSigWallet)).call{value: rewards}("");
require(success, "Transfer failed");
+
+ return address(multiSigWallet);
}
test/testLikeRegistry.t.sol
function testMatchMiscalculation() public {
uint256 likeAmount = 1 ether;
uint256 FIXEDFEE = 10;
uint256 fees = ((likeAmount + likeAmount) * FIXEDFEE) / 100;
uint multiSigFund = likeAmount + likeAmount - fees;
vm.prank(user);
soulboundNFT.mintProfile("Alice", 25, "ipfs://profileImage");
uint256 userTokenId = soulboundNFT.profileToToken(user);
assertEq(userTokenId, 1, "Token ID should be 1");
vm.prank(user2);
soulboundNFT.mintProfile("Bob", 27, "ipfs://profileImage");
uint256 user2TokenId = soulboundNFT.profileToToken(user2);
assertEq(user2TokenId, 2, "Token ID should be 2");
assertEq(address(likeRegistry).balance, 0);
vm.prank(user);
likeRegistry.likeUser{value: likeAmount}(user2);
vm.prank(user2);
address multiSigAddress = likeRegistry.likeUser{value: likeAmount}(user);
assert(multiSigAddress != address(0));
vm.prank(user);
address[] memory userMatches = likeRegistry.getMatches();
assertEq(userMatches.length, 1);
assertEq(userMatches[0], address(user2));
assertEq(address(multiSigAddress).balance, multiSigFund);
assertEq(address(likeRegistry).balance, fees);
}
Impact
The user sending fund to LikeRegistry::likeUser
has its fund never accounted to, which make a financial losses to the user
The new created MultisigWallet has no fund in it
Protocol unable to claim the protocol fee with LikeRegistry::withdrawFees
despite the protocol balance increases
Recommendations
Account user fund when user called LikeRegistry::likeUser
to a storage variable.
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");
@@ -38,16 +38,23 @@ contract LikeRegistry is Ownable {
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);
}
}