Description
When there is a mutual like, the LikeRegistry::matchRewards function is supposed to be called by the LikeRegistry::likeUser function, which is deploying a MultiSigWallet contract for the matched users. But, the address of the deployed MultiSigWallet contract is not being returned, emitted or stored anywhere, making it impossible for the underlying owners (i.e., matched users) of the MultiSigWallet contract to access / transfer the funds available in that contract.
Impact
The matched users wouldn't be able to access the funds available inside their MultiSigWallet contract.
Proof of Concept
-
User (say Bob) executes the LikeRegistry::likeUser function, passing the address of the liked user (say Alice).
-
The liked user (i.e., Alice) also executes the LikeRegistry::likeUser function passing the address of the user (i.e., Bob) who liked them.
-
Now, since the like is mutual, when the liked user (i.e., Alice) executes the LikeRegistry::likeUser function, the likes[liked][msg.sender] value will be true, triggering the call to the LikeRegistry::matchRewards function.
-
Inside the LikeRegistry::matchRewards function, the MultiSigWallet contract is deployed and the rewards are transferred to the MultiSigWallet contract.
-
The address of the deployed MultiSigWallet contract remains unknown because it's nether returned, emitted or stored anywhere.
-
The owners of the deployed MultiSigWallet contract are the underlying matched users. However, they can't access the funds inside the MultiSigWallet contract because there is no way to interact with it without knowing its address.
Proof of Code
Create a test file named as DatingDappTest.t.sol inside the test folder with the following content:
pragma solidity 0.8.20;
import {Test} from "forge-std/Test.sol";
import {LikeRegistry} from "../src/LikeRegistry.sol";
import {SoulboundProfileNFT} from "../src/SoulboundProfileNFT.sol";
import {MultiSigWallet} from "../src/MultiSig.sol";
contract DatingDappTest is Test {
SoulboundProfileNFT soulboundNFT;
LikeRegistry likeRegistry;
address owner = makeAddr("owner");
address bob = makeAddr("bob");
address alice = makeAddr("alice");
function setUp() public {
vm.startPrank(owner);
soulboundNFT = new SoulboundProfileNFT();
likeRegistry = new LikeRegistry(address(soulboundNFT));
vm.stopPrank();
}
function testUnableToFetchMultiSigWalletAddress() public {
vm.prank(bob);
soulboundNFT.mintProfile("Bob", 25, "ipfs://profileImage");
vm.prank(alice);
soulboundNFT.mintProfile("Alice", 24, "ipfs://profileImage");
hoax(bob, 1 ether);
likeRegistry.likeUser{value: 1 ether}(alice);
hoax(alice, 1 ether);
likeRegistry.likeUser{value: 1 ether}(bob);
}
}
Recommended Mitigation
The easiest recommendation would be to return the address of the deployed MultiSigWallet contract by adding the return statement at the end of the LikeRegistry::matchRewards function followed by assigning the same inside the mutual like condition of the LikeRegistry::likeUser function.
function likeUser(address liked) external payable returns(address matchToMultiSigWallet) {
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);
matchToMultiSigWallet = matchRewards(liked, msg.sender);
}
}
function matchRewards(address from, address to) internal returns(address) {
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");
return address(multiSigWallet);
}
Now, you can get the address of the deployed MultiSigWallet contract in the above DatingDappTest as:
function testFetchMultiSigWalletAddress() public {
vm.prank(bob);
soulboundNFT.mintProfile("Bob", 25, "ipfs://profileImage");
vm.prank(alice);
soulboundNFT.mintProfile("Alice", 24, "ipfs://profileImage");
hoax(bob, 1 ether);
address multiSigForMatch = likeRegistry.likeUser{value: 1 ether}(alice);
assertEq(multiSigForMatch, address(0), "There is no match because it's only one-sided like till now");
hoax(alice, 1 ether);
multiSigForMatch = likeRegistry.likeUser{value: 1 ether}(bob);
assertNotEq(multiSigForMatch, address(0), "There is a match because it's a mutual like now");
}
Or, you could also create a mapping to store the MultiSigWallet contract address corresponding to the combined hash of the addresses of the matched users, like:
mapping(bytes32 matchHash => address multiSigWallet) public matchToMultiSigWallet;
And, add the following line inside the LikeRegistry::matchRewards function:
matchToMultiSigWallet[keccak256(abi.encode(from, to))] = address(multiSigWallet);
So, the above testFetchMultiSigWalletAddress test function can be modified as:
function testFetchMultiSigWalletAddress() public {
vm.prank(bob);
soulboundNFT.mintProfile("Bob", 25, "ipfs://profileImage");
vm.prank(alice);
soulboundNFT.mintProfile("Alice", 24, "ipfs://profileImage");
hoax(bob, 1 ether);
likeRegistry.likeUser{value: 1 ether}(alice);
hoax(alice, 1 ether);
likeRegistry.likeUser{value: 1 ether}(bob);
address multiSigWalletAddress = likeRegistry.matchToMultiSigWallet(keccak256(abi.encode(bob, alice)));
assertNotEq(multiSigWalletAddress, address(0), "There is a match because it's a mutual like");
}