Summary
The contract requires users to send ETH (1 ETH per like), but due to a flaw in how fees and balances are managed, this ETH becomes inaccessible.
Vulnerability Details
The fee never increased in the matchRewards
function, the same as the user balance, of users were not tracked. This will lead to zero(0) fees at all times.
The withdraw function only withdraws the total fees accumulated, which in this case will be zero and always revert.
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 Fund Loss: ETH sent by users remains stuck in the contract forever.
Contract Owner Cannot Recover Fees: The business model fails because fees are never collected.
Smart Contract Becomes Non-Functional: Users will abandon the contract as there’s no benefit to interacting with it.
POC
function test_WithdrawIsZero_WhenOwner_Withdraw_and_Funds_Still_InContract() public {
vm.prank(user);
likeRegistry.likeUser{value: 1 ether}(user2);
assertTrue(likeRegistry.likes(user, user2));
vm.prank(user2);
likeRegistry.likeUser{value: 1 ether}(user);
assertTrue(likeRegistry.likes(user2, user));
uint256 userOneBalance = likeRegistry.userBalances(user);
uint256 userTwoBalance = likeRegistry.userBalances(user2);
assertEq(userOneBalance, 0);
assertEq(userTwoBalance, 0);
console.log("-------- Before Fee Withdraw --------");
console.log("Balance: ", address(likeRegistry).balance / 1e18);
vm.prank(owner);
vm.expectRevert("No fees to withdraw");
likeRegistry.withdrawFees();
console.log("-------- After Fee Withdraw --------");
console.log("Balance: ", address(likeRegistry).balance / 1e18);
}
Ran 1 test for test/testLikeRegistry.t.sol:SoulboundProfileNFTTest
[PASS] test_WithdrawIsZero_WhenOwner_Withdraw_and_Funds_Still_InContract() (gas: 722357)
Logs:
-------- Before Fee Withdraw --------
Balance: 2
-------- After Fee Withdraw --------
Balance: 2
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.16ms (1.96ms CPU time)
Ran 1 test suite in 388.15ms (3.16ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
We can see that the funds are stuck in the contract, and the withdrawFee
reverts to No fees to withdraw
Tools Used
Manual Review and Foundry
Recommendations
Modify likeUser
to store user deposits correctly
userBalances[msg.sender] += msg.value;
Fix Fee Calculation
uint256 matchingFees = (userBalances[from] + userBalances[to]) * FIXEDFEE / 100;
totalFees += matchingFees;
Implement an withdrawal to Withdraw entire contract ETH balance
function withdrawFees() external onlyOwner {
(bool success, ) = payable(owner()).call{value: address(this).balance}("");
require(success, "Withdraw failed");
}