Beginner FriendlyFoundryDeFi
100 EXP
View results
Submission Details
Severity: high
Valid

`Steaking:depositIntoVault` fails to update the users balance allowing for excess deposits

Summary

The depositIntoVault function fails to update the users balance in the usersToStakes array, this allows a user to call depositIntoVault multiple times, depositing their staked amount into the vault and receiving shares for that amount each time.

Vulnerability Details (POC)

as we can see in the function depositIntoVault :

@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)
sharesReceived: uint256 = extcall IWETHSteakVault(self.vault).deposit(stakedAmount, msg.sender)
log DepositedIntoVault(msg.sender, stakedAmount, sharesReceived)
return sharesReceived

The stakedAmount is used to calculate the amount of shares received but fails to update this state before sending the amount to the vault.

In Steaking.t.sol add a second user to the setup:

contract SteakingTest is Test, EventsAndErrors {
address public owner;
address public user1;
+ address public user2;
ISteaking public steaking;
MockWETH public weth;
MockWETHSteakVault public wethSteakVault;
function setUp() public {
string memory path = "src/Steaking.vy";
owner = makeAddr("owner");
user1 = makeAddr("user1");
+ user2 = makeAddr("user2");

Then add the sollowing test:

function testDepositIntoVaultCanBeCalledMultipleTimes() public {
// deal users funds
uint256 dealAmount = 1 ether;
uint256 dealAmount2 = 2 ether;
vm.deal(user1, dealAmount);
vm.deal(user2, dealAmount2);
// users stake
_stake(user1, dealAmount, user1);
_stake(user2, dealAmount2, user2);
// end staking period
_endStakingPeriod();
// set vault address
vm.startPrank(owner);
steaking.setVaultAddress(address(wethSteakVault));
vm.stopPrank();
// deposit into vault
vm.startPrank(user1);
steaking.depositIntoVault();
vm.stopPrank();
// user calls again but shouldnt be able to
vm.startPrank(user1);
steaking.depositIntoVault();
vm.stopPrank();
// user2 calls but will fail due to insufficient funds in contract
vm.startPrank(user2);
vm.expectRevert();
steaking.depositIntoVault();
vm.stopPrank();
// check user1s balance in the vault
uint256 wethSteakVaultShares = wethSteakVault.balanceOf(user1);
assertEq(wethSteakVaultShares, dealAmount2);
}

In this scenario 2 users have staked to the protocol.

  • user1 stakes 1 ETH

  • user2 stakes 2 ETH

  • staking period ends

  • user1 deposits to the vault for a value of 1 ETH

  • user1 calls deposit again for a value of 1 ETH

  • user2 calls deposit for a value of 2 ETH, but fails due to the contract only having 1 ETH of balance

Impact

A user can call this function multiple times, denying other users of a claim to their shares.

Tools Used

Foundry, Manual review

Recommendations

Update the state of the users Balance after getting the sharesAmount value for the deposit.

@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]
+ self.usersToStakes[msg.sender] -= stakedAmount
assert stakedAmount > 0, STEAK__AMOUNT_ZERO
extcall IWETH(WETH).deposit(value=stakedAmount)
extcall IWETH(WETH).approve(self.vault, stakedAmount)
sharesReceived: uint256 = extcall IWETHSteakVault(self.vault).deposit(stakedAmount, msg.sender)
log DepositedIntoVault(msg.sender, stakedAmount, sharesReceived)
return sharesReceived
Updates

Lead Judging Commences

inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Validated
Assigned finding tags:

`Steaking:depositIntoVault` fails to update the users balance allowing contract draining to repeat call

Support

FAQs

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