Description
The LikeRegistry contract contains a critical vulnerability where ETH sent directly to the contract becomes permanently locked. While the contract includes a receive() function to accept ETH, it lacks any mechanism to withdraw these funds, as the only withdrawal function withdrawFees() is limited to fee collection.
function withdrawFees() external onlyOwner {
require(totalFees > 0, "No fees to withdraw");
uint256 totalFeesToWithdraw = totalFees;
totalFees = 0;
(bool success,) = payable(owner()).call{value: totalFeesToWithdraw}("");
require(success, "Transfer failed");
}
Impact:
Permanent loss of user funds sent directly to contract
No recovery mechanism for mistaken transfers
Contract balance can diverge from tracked fees
Owner cannot recover excess funds
Proof of Concept:
The following test demonstrates how funds can become permanently locked:
function testLockedFunds() public {
vm.deal(user, 5 ether);
vm.prank(user);
(bool sent,) = address(likeRegistry).call{value: 2 ether}("");
require(sent, "Failed to send ETH");
assertEq(address(likeRegistry).balance, 2 ether);
vm.expectRevert("No fees to withdraw");
likeRegistry.withdrawFees();
vm.mockCall(
address(likeRegistry),
abi.encodeWithSelector(likeRegistry.totalFees.selector),
abi.encode(1 ether)
);
assertEq(address(likeRegistry).balance, 2 ether);
vm.stopPrank();
}
function testMultipleLockedScenarios() public {
vm.deal(user, 5 ether);
vm.deal(user2, 5 ether);
vm.prank(user);
soulboundNFT.mintProfile("Alice", 25, "ipfs://Alice");
vm.prank(user2);
soulboundNFT.mintProfile("Bob", 28, "ipfs://Bob");
vm.prank(user);
likeRegistry.likeUser{value: 2 ether}(user2);
vm.prank(user2);
(bool sent,) = address(likeRegistry).call{value: 1 ether}("");
require(sent, "Failed to send ETH");
assertEq(address(likeRegistry).balance, 3 ether);
vm.expectRevert();
(sent,) = address(likeRegistry).call{value: 0}(
abi.encodeWithSignature("withdrawBalance()")
);
}
Fix Recommendation:
mapping(address => uint256) public directDeposits;
receive() external payable {
directDeposits[msg.sender] += msg.value;
emit DirectDepositReceived(msg.sender, msg.value);
}
function withdrawDirectDeposit() external {
uint256 amount = directDeposits[msg.sender];
require(amount > 0, "No direct deposits");
directDeposits[msg.sender] = 0;
(bool success,) = payable(msg.sender).call{value: amount}("");
require(success, "Refund failed");
emit DirectDepositWithdrawn(msg.sender, amount);
}
Tools Used
Foundry Testing Framework
Manual Review