Root + Impact
Description
When a mutual match occurs in LikeRegistry.sol, the matchRewards function deploys a new MultiSigWallet contract. However, the address of this newly deployed contract is only stored in a local stack variable and is never saved to the contract's state or emitted in the Matched event.
Impact
Permanent Loss of Funds: Rewards are successfully transferred to the new MultiSigWallet address, but since that address is not recorded anywhere on-chain, the matched users (the owners) have no way to locate the contract to interact with it.
Locked Liquidity: Any ETH rewards sent to the wallet remain permanently locked because no one can call submitTransaction or approveTransaction without the contract address.
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");
}
Risk
Likelihood:
Users receive a Matched event but have no way to retrieve the address of the MultiSig contract. Any ETH sent to the MultiSig as a reward remains permanently inaccessible to the owners.
Proof of Concept
This Foundry test proves that while the system "performs" the match and moves funds, it fails to provide a way for users to "spot" the resulting contract. The test verifies that:
The Matched event contains only user addresses and lacks the wallet address.
The LikeRegistry balance drops to zero, confirming the funds were sent to an unrecorded destination.
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../src/LikeRegistry.sol";
import "../src/SoulboundProfileNFT.sol";
import "../src/MultiSig.sol";
contract LikeRegistryPoC is Test {
LikeRegistry registry;
SoulboundProfileNFT nft;
address user1 = makeAddr("Adarsh");
address user2 = makeAddr("User2");
function setUp() public {
nft = new SoulboundProfileNFT();
registry = new LikeRegistry(address(nft));
vm.deal(user1, 10 ether);
vm.deal(user2, 10 ether);
vm.prank(user1);
nft.mintProfile("Adarsh", 25, "ipfs://img1");
vm.prank(user2);
nft.mintProfile("User2", 24, "ipfs://img2");
}
function test_FundsAreLockedBecauseAddressIsLost() public {
vm.prank(user1);
registry.likeUser{value: 1 ether}(user2);
vm.recordLogs();
vm.prank(user2);
registry.likeUser{value: 1 ether}(user1);
Vm.Log[] memory entries = vm.getRecordedLogs();
assertEq(entries[1].topics.length, 3, "Event should have exactly 3 topics (missing wallet)");
assertEq(address(registry).balance, 0, "Funds left registry but destination is unknown");
}
}
Recommended Mitigation
To fix this, we must ensure the deployment address is captured and persisted. The mitigation involves:
State Storage: Adding a mapping to store the wallet address for each pair of users.
Event Transparency: Updating the Matched event to include the new contract address.
Balance Tracking: Correctly updating userBalances during the likeUser call so the rewards are accurately calculated.
+ mapping(address => mapping(address => address)) public getMultiSigWallet;
- event Matched(address indexed user1, address indexed user2);
+ event Matched(address indexed user1, address indexed user2, address multiSigAddress);
function matchRewards(address from, address to) internal {
MultiSigWallet multiSigWallet = new MultiSigWallet(from, to);
+ address walletAddr = address(multiSigWallet);
+ getMultiSigWallet[from][to] = walletAddr;
+ getMultiSigWallet[to][from] = walletAddr;
+ emit Matched(from, to, walletAddr);
- (bool success,) = payable(address(multiSigWallet)).call{value: rewards}("");
+ (bool success,) = payable(walletAddr).call{value: rewards}("");
require(success, "Transfer failed");
}