Summary
The protocol requires users to pay 1 ETH when liking another profile. For mutual matches, all previously sent ETH (minus 10% fees) should be pooled into a MultiSig wallet. However, the LikeRegistry
contract fails to track user ETH contributions in the userBalances
mapping during the LikeRegistry::likeUser
function. As a result, mutual matches create empty MultiSig wallets with 0 ETH, rendering the core reward mechanism non-functional.
Vulnerability Details
The issue arises due to the contract not updating the userBalances
mapping after receiving ETH payments. Since these contributions are never recorded, the matchRewards
function attempts to distribute rewards from an empty balance, preventing users from receiving their pooled ETH rewards.
src/LikeRegistry.sol#L31-L48
function likeUser(address liked) external payable {
require(msg.value >= 1 ether, "Must send at least 1 ETH");
likes[msg.sender][liked] = true;
emit Liked(msg.sender, liked);
if (likes[liked][msg.sender]) {
matchRewards(liked, msg.sender);
}
}
To confirm the presence of this issue, the following test case verifies that userBalances
is never updated after a like action.
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../src/SoulboundProfileNFT.sol";
import "../src/LikeRegistry.sol";
import "../src/MultiSig.sol";
contract DatingDappTest is Test {
SoulboundProfileNFT public profileNFT;
LikeRegistry public likeRegistry;
address public alice = makeAddr("alice");
address public bob = makeAddr("bob");
uint256 public constant LIKE_FEE = 1 ether;
function setUp() public {
profileNFT = new SoulboundProfileNFT();
likeRegistry = new LikeRegistry(address(profileNFT));
vm.prank(alice);
profileNFT.mintProfile("Alice", 25, "ipfs://alice");
vm.prank(bob);
profileNFT.mintProfile("Bob", 28, "ipfs://bob");
}
function testMissingBalanceTracking() public {
vm.deal(alice, LIKE_FEE);
vm.prank(alice);
likeRegistry.likeUser{value: LIKE_FEE}(bob);
assertEq(
likeRegistry.userBalances(alice),
0,
"Alice's balance should be 0"
);
vm.deal(bob, LIKE_FEE);
vm.prank(bob);
likeRegistry.likeUser{value: LIKE_FEE}(alice);
vm.prank(bob);
address[] memory matches = likeRegistry.getMatches();
require(matches.length > 0, "No matches found for Bob");
address payable multiSigAddr = payable(matches[0]);
MultiSigWallet multiSig = MultiSigWallet(multiSigAddr);
assertEq(address(multiSig).balance, 0, "MultiSig should have 0 ETH");
assertEq(
likeRegistry.userBalances(bob),
0,
"Bob's balance should be 0"
);
}
}
Expected Test Output
$ forge test --mt testMissingBalanceTracking
[⠰] Compiling...
[⠔] Installing Solc version 0.8.26
[⠑] Successfully installed Solc 0.8.26
No files changed, compilation skipped
Ran 1 test for test/DatingDappTest.t.sol:DatingDappTest
[PASS] testMissingBalanceTracking() (gas: 719242)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 858.20µs (168.45µs CPU time)
Ran 1 test suite in 12.30ms (858.20µs CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
Impact
Matched users receive empty MultiSig wallets instead of pooled ETH rewards. Since user balances are never recorded, their ETH payments and potential fees are effectively lost, with no mechanism to recover the funds. This completely breaks the intended incentive structure, making the reward system non-functional.
Tools Used
Foundry
Recommendations
Modify the LikeRegistry::likeUser
function to correctly track ETH contributions. The following fix ensures that user balances are updated properly:
function likeUser(address liked) external payable {
require(msg.value >= 1 ether, "Must send at least 1 ETH");
// ...
likes[msg.sender][liked] = true;
+ userBalances[msg.sender] += msg.value; // Track ETH contribution
emit Liked(msg.sender, liked);
if (likes[liked][msg.sender]) {
// ...
}
}
By adding userBalances[msg.sender] += msg.value;
, the contract correctly tracks ETH contributions, ensuring mutual matches result in properly funded MultiSig wallets.