Summary
In the LikeRegistry
contract, the matchRewards()
function fails to update user balances properly, causing the reward distribution to always fail. The ETH deposited by users in likeUser()
is not stored in the userBalances
mapping, meaning that when users match, the matchRewards()
function will calculate the reward as 0
. This issue prevents users from receiving their rewards and their funds are locked forever in the contract.
Vulnerability Details
Affected code
The LikeRegistry.sol::likeUser()
function allows users to send ETH when liking another user. However, the userBalances
mapping, which should store the amount of ETH a user deposits, is never updated. As a result, when the matchRewards()
function is called, the total rewards are always calculated as 0
, and no ETH is transferred to the newly deployed MultiSig wallet. This issue effectively breaks the core functionality of the protocol, as users are not receiving the ETH rewards from matching and their funds are locked forever.
PoC
Add the following test suit to the SoulboundProfileNFTTest.t.sol
and modify the setUp()
.
import "forge-std/Test.sol";
import "../src/SoulboundProfileNFT.sol";
import "../src/LikeRegistry.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
contract SoulboundProfileNFTTest is Test {
SoulboundProfileNFT soulboundNFT;
address user = address(0x123);
address user2 = address(0x456);
address owner = address(this);
LikeRegistry private likeRegistry;
function setUp() public {
soulboundNFT = new SoulboundProfileNFT();
likeRegistry = new LikeRegistry(address(soulboundNFT));
}
function test_matchRewardsFunctionWillAlwaysSendZeroRewards() public {
vm.prank(user);
vm.deal(user, 1 ether);
soulboundNFT.mintProfile("Alice", 25, "ipfs://profileImage");
vm.prank(user2);
vm.deal(user2, 1 ether);
soulboundNFT.mintProfile("Bob", 25, "ipfs://profileImage");
vm.prank(user);
likeRegistry.likeUser{value: 1 ether}(user2);
vm.prank(user2);
likeRegistry.likeUser{value: 1 ether}(user);
assertEq(address(likeRegistry).balance, 2 ether);
}
}
Ran 1 test for test/testSoulboundProfileNFT.t.sol:SoulboundProfileNFTTest
[PASS] test_matchRewardsFunctionWillAlwaysSendZeroRewards() (gas: 1018803)
Traces:
[1018803] SoulboundProfileNFTTest::test_matchRewardsFunctionWillAlwaysSendZeroRewards()
├─ [0] VM::prank(0x0000000000000000000000000000000000000123)
│ └─ ← [Return]
├─ [0] VM::deal(0x0000000000000000000000000000000000000123, 1000000000000000000 [1e18])
│ └─ ← [Return]
├─ [163928] SoulboundProfileNFT::mintProfile("Alice", 25, "ipfs://profileImage")
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0x0000000000000000000000000000000000000123, tokenId: 1)
│ ├─ emit ProfileMinted(user: 0x0000000000000000000000000000000000000123, tokenId: 1, name: "Alice", age: 25, profileImage: "ipfs://profileImage")
│ └─ ← [Stop]
├─ [0] VM::prank(0x0000000000000000000000000000000000000456)
│ └─ ← [Return]
├─ [0] VM::deal(0x0000000000000000000000000000000000000456, 1000000000000000000 [1e18])
│ └─ ← [Return]
├─ [142028] SoulboundProfileNFT::mintProfile("Bob", 25, "ipfs://profileImage")
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0x0000000000000000000000000000000000000456, tokenId: 2)
│ ├─ emit ProfileMinted(user: 0x0000000000000000000000000000000000000456, tokenId: 2, name: "Bob", age: 25, profileImage: "ipfs://profileImage")
│ └─ ← [Stop]
├─ [0] VM::prank(0x0000000000000000000000000000000000000123)
│ └─ ← [Return]
├─ [31029] LikeRegistry::likeUser{value: 1000000000000000000}(0x0000000000000000000000000000000000000456)
│ ├─ [630] SoulboundProfileNFT::profileToToken(0x0000000000000000000000000000000000000123) [staticcall]
│ │ └─ ← [Return] 1
│ ├─ [630] SoulboundProfileNFT::profileToToken(0x0000000000000000000000000000000000000456) [staticcall]
│ │ └─ ← [Return] 2
│ ├─ emit Liked(liker: 0x0000000000000000000000000000000000000123, liked: 0x0000000000000000000000000000000000000456)
│ └─ ← [Stop]
├─ [0] VM::prank(0x0000000000000000000000000000000000000456)
│ └─ ← [Return]
├─ [646796] LikeRegistry::likeUser{value: 1000000000000000000}(0x0000000000000000000000000000000000000123)
│ ├─ [630] SoulboundProfileNFT::profileToToken(0x0000000000000000000000000000000000000456) [staticcall]
│ │ └─ ← [Return] 2
│ ├─ [630] SoulboundProfileNFT::profileToToken(0x0000000000000000000000000000000000000123) [staticcall]
│ │ └─ ← [Return] 1
│ ├─ emit Liked(liker: 0x0000000000000000000000000000000000000456, liked: 0x0000000000000000000000000000000000000123)
│ ├─ emit Matched(user1: 0x0000000000000000000000000000000000000456, user2: 0x0000000000000000000000000000000000000123)
│ ├─ [491245] → new MultiSigWallet@0xffD4505B3452Dc22f8473616d50503bA9E1710Ac
│ │ └─ ← [Return] 2230 bytes of code
│ ├─ [55] MultiSigWallet::receive()
│ │ └─ ← [Stop]
│ └─ ← [Stop]
├─ [0] VM::assertEq(2000000000000000000 [2e18], 2000000000000000000 [2e18]) [staticcall]
│ └─ ← [Return]
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.08ms (355.59µs CPU time)
Impact
Users are unable to receive the ETH they deposit when they match with another user.
This issue affects the entire reward distribution mechanism and renders the matching functionality useless.
The protocol will not be able to fulfill its intended purpose of transferring rewards to users’ MultiSig wallets.
Users lose their trust of the protocol.
Tools Used
Recommendations
In the likeUser()
function, update the userBalances
mapping to store the ETH deposited by users.
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");
require(profileNFT.profileToToken(msg.sender) != 0, "Must have a profile NFT");
require(profileNFT.profileToToken(liked) != 0, "Liked user must have a profile NFT");
userBalances[msg.sender] += msg.value;
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);
matchRewards(liked, msg.sender);
}
}