Description:
The steaking contract lacks a fallback function to handle ETH sent directly to the steaking contract.
Impact:
Users may lose their funds if they mistakenly send ETH directly to the steaking contract.
Proof of Concept:
- Userinitial balance is 8 ether.
 
- Userjust sends- 2 etherdirectly to the- steaking contract.
 
- Userloses his- 2 etherwithin the- steakingcontract since it doesn't have a fallback function to redirect the ETH to the- stakefunction.
 
- Loss of funds is evident since it is not recorded in the - userToStakesmapping and the- totalAmountStakedvariable.
 
Code
Place the following into Steaking.t.sol
function testUserSendsETHDirect() public {
       vm.deal(user1, 8 ether);
        vm.startPrank(user1);
        (bool sent, bytes memory data) = address(steaking).call{value: 2 ether}("");
        vm.stopPrank();
        uint256 userStakedAmount = steaking.usersToStakes(user1);
        uint256 totalAmountStaked = steaking.totalAmountStaked();
        console2.log("This is the total staked Amount of User1::", userStakedAmount);
        console2.log("This is the total amount staked in the contract::", totalAmountStaked);
        require(sent, "Failed to send Ether directly to contract");
        assert(userStakedAmount == 2 ether);
        assert(totalAmountStaked == 2 ether);
        assert(address(user1).balance == 6 ether);
    }
 
Here are the logs that were shown.
[27733] SteakingTest::testUserSendsETHDirect()
    ├─ [0] VM::deal(user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF], 8000000000000000000 [8e18])
    │   └─ ← [Return] 
    ├─ [0] VM::startPrank(user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF])
    │   └─ ← [Return] 
    ├─ [117] Steaking::fallback{value: 2000000000000000000}()
    │   └─ ← [Revert] EvmError: Revert
    ├─ [0] VM::stopPrank()
    │   └─ ← [Return] 
    ├─ [2360] Steaking::usersToStakes(user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF]) [staticcall]
    │   └─ ← [Return] 0
    ├─ [2223] Steaking::totalAmountStaked() [staticcall]
    │   └─ ← [Return] 0
    ├─ [0] console::log("This is the total staked Amount of User1::", 0) [staticcall]
    │   └─ ← [Stop] 
    ├─ [0] console::log("This is the total amount staked in the contract::", 0) [staticcall]
    │   └─ ← [Stop] 
    └─ ← [Revert] revert: Failed to send Ether directly to contract
As we can see, the steaking contract lacks a fallback function to handle the direct ETH transfer from a user. The ETH tragically gets lost in the steaking contract since it cannot be recorded in the userToStakes mapping which records 0 and also the totalAmountStaked variable which also records 0.
Recommended Mitigation:
Implement a fallback function to manage direct ETH transfers.
Here is the recommended fix.
+ @external
+ @payable
+ def __default__():
+    self._stake(msg.sender)
    
+ @internal
+ @payable
+ def _stake(_onBehalfOf: address):
+    """
+    @notice Internal function to handle staking logic.
+    """
+    assert not self._hasStakingPeriodEnded(), STEAK__STAKING_PERIOD_ENDED
+    assert msg.value >= MIN_STAKE_AMOUNT, STEAK__INSUFFICIENT_STAKE_AMOUNT
+    assert _onBehalfOf != ADDRESS_ZERO, STEAK__ADDRESS_ZERO
+    self.usersToStakes[_onBehalfOf] += msg.value
+    self.totalAmountStaked += msg.value
+    log Staked(msg.sender, msg.value, _onBehalfOf)
- def stake(_onBehalfOf: address):
-     """
-     @notice Allows users to stake ETH for themselves or any other user within the staking period.
-     @param _onBehalfOf The address to stake on behalf of.
-     """
-     assert not self._hasStakingPeriodEnded(), STEAK__STAKING_PERIOD_ENDED
-     assert msg.value >= MIN_STAKE_AMOUNT, STEAK__INSUFFICIENT_STAKE_AMOUNT
-     assert _onBehalfOf != ADDRESS_ZERO, STEAK__ADDRESS_ZERO
-     self.usersToStakes[_onBehalfOf] = msg.value
-     self.totalAmountStaked += msg.value
-     log Staked(msg.sender, msg.value, _onBehalfOf)
+ @external
+ @payable
+ def stake(_onBehalfOf: address):
+    """
+    @notice External function that calls the internal staking logic.
+    """
+    self._stake(_onBehalfOf)
The stake function is split into an internal _stake function and an external stake function. The internal function can be called directly by the __default__ function.
This changes may make this test that was shared in the Proof of Concept section pass successfully as it is evident in the logs.
[72443] SteakingTest::testUserSendsETHDirect()
    ├─ [0] VM::deal(user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF], 8000000000000000000 [8e18])
    │   └─ ← [Return] 
    ├─ [0] VM::startPrank(user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF])
    │   └─ ← [Return] 
    ├─ [48646] Steaking::fallback{value: 2000000000000000000}()
    │   ├─ emit Staked(by: user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF], amount: 2000000000000000000 [2e18], onBehalfOf: user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF])
    │   └─ ← [Stop] 
    ├─ [0] VM::stopPrank()
    │   └─ ← [Return] 
    ├─ [360] Steaking::usersToStakes(user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF]) [staticcall]
    │   └─ ← [Return] 2000000000000000000 [2e18]
    ├─ [223] Steaking::totalAmountStaked() [staticcall]
    │   └─ ← [Return] 2000000000000000000 [2e18]
    ├─ [0] console::log("This is the total staked Amount of User1::", 2000000000000000000 [2e18]) [staticcall]
    │   └─ ← [Stop] 
    ├─ [0] console::log("This is the total amount staked in the contract::", 2000000000000000000 [2e18]) [staticcall]
    │   └─ ← [Stop] 
    └─ ← [Stop] 
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 23.94ms (2.84ms CPU time)