Summary
Withdrawing of a correspondingly smaller amount of funds than previously staked by the user results in staking an amount below the required minimum (less than 0.5 ETH).
Vulnerability Details
The Steaking::unstake
function allows to withdraw staked assets of the user during staking period. Users can use it to adjust their staked ETH amount, or withdraw it completely. The additional requirement is that minimum staked amount is 0.5
ether. It is possible to broke this invariant when user first stake specific amount of ether and then withdraw a little less, where (staked amount - withdrawn amount) < 0.5 ether.
Impact
The minimum staked amount requirement could be broken.
Proof of Concepts
user stakes the specific amount of assets, greater or equal to the minimum requirement.
user withdraws a little less than the staked amount before the staking period ends.
Proof of code
Add the following code to the Steaking.t.sol
file within the SteakingTest
contract.
function testUnstakeBelowMinimumStakedAmount() public {
uint256 user1StakedAmount;
uint256 dealAmount = 1 ether;
uint256 unstakeAmount = 0.8 ether;
_stake(user1, dealAmount, user1);
user1StakedAmount = steaking.usersToStakes(user1);
assertEq(user1StakedAmount, dealAmount);
_unstake(user1, unstakeAmount, user1);
user1StakedAmount = steaking.usersToStakes(user1);
assertEq(user1StakedAmount, dealAmount - unstakeAmount);
assertLt(user1StakedAmount, 0.5 ether);
}
Tools Used
Recommended Mitigation
The Steaking::unstake
should check that the remaining amount (staked amount - withdrawn amount) is either greater than the minimum staked value (0.5 ETH) or equal to 0.
Possible solution (changes in Steaking.vy
file):
+STEAK__STEAK_AMOUNT_LESS_THAN_MINIMUM: constant(String[33]) = "Steak__SteakAmountLessThanMinimum"
def unstake(_amount: uint256, _to: address):
"""
@notice Allows users to unstake their staked ETH before the staking period ends. Users
can adjust their staking amounts to their liking.
@param _amount The amount of staked ETH to withdraw.
@param _to The address to send the withdrawn ETH to.
"""
assert not self._hasStakingPeriodEnded(), STEAK__STAKING_PERIOD_ENDED
assert _to != ADDRESS_ZERO, STEAK__ADDRESS_ZERO
stakedAmount: uint256 = self.usersToStakes[msg.sender]
assert stakedAmount > 0 and _amount > 0, STEAK__AMOUNT_ZERO
+ remainingAmount: uint256 = stakedAmount - _amount
+ assert remainingAmount == 0 or remainingAmount >= MIN_STAKE_AMOUNT, STEAK__STEAK_AMOUNT_LESS_THAN_MINIMUM
assert _amount <= stakedAmount, STEAK__INSUFFICIENT_STAKE_AMOUNT
self.usersToStakes[msg.sender] -= _amount
self.totalAmountStaked -= _amount
send(_to, _amount)
log Unstaked(msg.sender, _amount, _to)