2025-02-datingdapp/audit/H-02.md
han 09ed9015e5
Some checks failed
CI / Foundry project (push) Has been cancelled
add valid finding, ai finding, and retro
2025-03-08 23:24:27 +07:00

177 lines
6.4 KiB
Markdown

# H-02. User Fund Sent to LikeRegistry::likeUser is Never Accounted and Make `LikeRegistry::matchRewards` Always Return Zero
## This is a valid finding. Written by myself.
## Summary
Due to miscalculation 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
```solidity
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;
// Deploy a MultiSig contract for the matched users
MultiSigWallet multiSigWallet = new MultiSigWallet(from, to);
// Send ETH to the deployed multisig wallet
(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:
```solidity
function likeUser(address liked) external payable {
[...]
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);
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`
```diff
- 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`
```solidity
- 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 {
// Send ETH to the deployed multisig wallet
(bool success,) = payable(address(multiSigWallet)).call{value: rewards}("");
require(success, "Transfer failed");
+
+ return address(multiSigWallet);
}
```
`test/testLikeRegistry.t.sol`
```solidity
function testMatchMiscalculation() public {
uint256 likeAmount = 1 ether;
uint256 FIXEDFEE = 10;
uint256 fees = ((likeAmount + likeAmount) * FIXEDFEE) / 100;
uint multiSigFund = likeAmount + likeAmount - fees;
// user mint profile
vm.prank(user);
soulboundNFT.mintProfile("Alice", 25, "ipfs://profileImage");
uint256 userTokenId = soulboundNFT.profileToToken(user);
assertEq(userTokenId, 1, "Token ID should be 1");
// user2 mint profile
vm.prank(user2);
soulboundNFT.mintProfile("Bob", 27, "ipfs://profileImage");
uint256 user2TokenId = soulboundNFT.profileToToken(user2);
assertEq(user2TokenId, 2, "Token ID should be 2");
// likeRegistry has 0 ether
assertEq(address(likeRegistry).balance, 0);
// user likes user2 with 1 eth
vm.prank(user);
likeRegistry.likeUser{value: likeAmount}(user2);
// user2 likes uer with 1 eth and match
vm.prank(user2);
address multiSigAddress = likeRegistry.likeUser{value: likeAmount}(user);
assert(multiSigAddress != address(0));
// get user matches
vm.prank(user);
address[] memory userMatches = likeRegistry.getMatches();
assertEq(userMatches.length, 1);
assertEq(userMatches[0], address(user2));
assertEq(address(multiSigAddress).balance, multiSigFund); // giving a like for each user 1 ether should give the wallet 1.8 ether
assertEq(address(likeRegistry).balance, fees); // likeRegistry should have balance equal to `fees`, and send the rest of fund to the MultisigWallet
}
```
## 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.
```diff
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);
}
}
```