Beginner FriendlyFoundryDeFi
100 EXP
View results
Submission Details
Severity: low
Invalid

Direct ETH transfers to the `steaking` contract may cause loss of funds.

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:

  1. User initial balance is 8 ether.

  2. User just sends 2 ether directly to the steaking contract.

  3. User loses his 2 ether within the steaking contract since it doesn't have a fallback function to redirect the ETH to the stake function.

  4. Loss of funds is evident since it is not recorded in the userToStakes mapping and the totalAmountStaked variable.

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)
Updates

Lead Judging Commences

inallhonesty Lead Judge 11 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.