The LikeRegistry
contract contains a critical issue where ETH sent via direct transfers is neither tracked nor recoverable. This results in an ever-growing pool of stuck ETH, which is inaccessible to both users and the contract owner. The issue stems from the receive()
function, which allows the contract to accept ETH but does not update any internal balance variables or provide a way to withdraw these funds.
The root cause of this issue lies in how the contract handles incoming ETH. The contract includes a receive()
function, which allows it to accept ETH sent via low-level calls. However, this ETH is not assigned to any specific variable or utilized within the contract’s logic.
This function ensures that any ETH sent to the contract is accepted, but it does nothing beyond that. Unlike ETH sent through likeUser()
, which is properly accounted for in userBalances
, ETH received via direct transfers is ignored by all internal tracking mechanisms. The withdrawFees()
function, which is the only method for retrieving contract-held ETH, exclusively relies on the totalFees
variable:
Since totalFees
is only updated within the matchRewards()
function, it does not account for ETH sent directly to the contract. As a result, any ETH transferred using send()
, transfer()
, or call()
without an associated function call remains locked within the contract indefinitely.
The test case testUntrackedETHGetsStuck()
proves this issue by sending ETH directly to the contract, verifying its presence in the contract balance, and confirming that withdrawFees()
is unable to retrieve it:
include this in the test suite
Below is the log of the output
ETH was successfully sent to the contract via receive()
The log shows LikeRegistry::receive{value: 1000000000000000000}() (1 ETH was sent)
.
No reverts occurred, meaning the contract accepted the ETH.
The contract's balance increased
VM::assertEq(1000000000000000000 [1e18], 1000000000000000000 [1e18], "ETH should be in LikeRegistry")
confirms that the contract's balance is now 1 ETH.
Calling totalFees() reverted
LikeRegistry::totalFees() [staticcall]
reverted, meaning the test tried to call totalFees() but failed.
This suggests totalFees is either private or not accessible via a public getter which aligns with the contract definition.
Total fees remained 0
VM::assertEq(0, 0, "Total fees should NOT include direct ETH transfers")
confirms that no ETH was tracked as fees.
withdrawFees() failed with "No fees to withdraw"
LikeRegistry::withdrawFees()
reverted with revert: No fees to withdraw, proving that the untracked ETH cannot be withdrawn.
Final Assertion Passed
VM::assertFalse(false, "Owner should NOT be able to withdraw untracked ETH")
confirms that the contract's owner cannot recover the ETH.
This confirms that ETH sent via direct transfers becomes trapped within the contract with no method of retrieval.
The presence of a receive()
function without corresponding tracking mechanisms can lead to a serious accumulation of stranded ETH. Over time, users may inadvertently send ETH to the contract, expecting it to be used in protocol operations, only for it to remain permanently inaccessible.
To prevent ETH from getting stuck in the contract, by modifying the receive()
function to contribute received ETH to totalFees
, ensuring it can be withdrawn using withdrawFees()
:
Not the best design, but if you send money accidentally, that's a user mistake. Informational.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.