xanoutas/solidity-security-findings huggingface dataset:
Users can block other users from redeeming their ETH in Vaults
Audit: 2022-11-stakehouse-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2022-11-stakehouse/blob/4b6828e9c807f2f7c569e6d721ca1289f7cf7112/contracts/liquid-staking/LPToken.sol#L68
https://github.com/code-423n4/2022-11-stakehouse/blob/4b6828e9c807f2f7c569e6d721ca1289f7cf7112/contracts/liquid-staking/SavETHVault.sol#L186


# Vulnerability details

The `burnLPToken` of a protected vault allow users to burn LP tokens in exchange of ETH or dETH.

In the case of ETH, ie when the BLS key has not had its derivatives minted yet, the function checks the liquidity is not fresh by checking `_lpToken.lastInteractedTimestamp(msg.sender)`

```solidity
140:         // before burning, check the last LP token interaction and make sure its more than 30 mins old before permitting ETH withdrawals
141:         bool isStaleLiquidity = _lpToken.lastInteractedTimestamp(msg.sender) + 30 minutes < block.timestamp;
```

This opens a DOS attack vector: a user can prevent another from calling `burnLPToken` by transferring them any amount (even 1 gwei worth of LP), as the `lastInteractedTimestamp` of the receiver is updated in every `LP._transfer()`:

```solidity
66:     function _afterTokenTransfer(address _from, address _to, uint256 _amount) internal override {
67:         lastInteractedTimestamp[_from] = block.timestamp;
68:         lastInteractedTimestamp[_to] = block.timestamp; //@audit - recipient timestamp updated in every transfer
```

Note that [the same grieving attack](https://github.com/code-423n4/2022-11-stakehouse/blob/4b6828e9c807f2f7c569e6d721ca1289f7cf7112/contracts/liquid-staking/ETHPoolLPFactory.sol#L82) can be performed for a user trying to call `rotateLPTokens()`.

The same issue is also present in `savETHVault` and the Giant pools.

## Impact

Medium

## Proof of Concept

- Alice and Bob both deposit `12 ETH` in a protected vault, receiving `LPTokens`
- After some time, Alice decides to withdraw her ETH by calling `burnLPToken()`
- Bob front-runs her call with a `LPToken.transfer(Alice, 1)` call
- `burnLPToken()` will revert because Alice's liquidity is too fresh.

Add this test to `SavETHVault.t.sol`, recreating the steps described above:

```solidity
function testBurnLPDOS() public {
      // First supply ETH when validator is at initials registered phase
      savETHVault.accountMan().setLifecycleStatus(blsPubKeyOne, 1);
      liquidStakingManager.setIsPartOfNetwork(blsPubKeyOne, true);
      
      uint256 stakeAmount = 12 ether;
      vm.deal(accountOne, stakeAmount);
      vm.deal(accountTwo, stakeAmount); 
      vm.warp(0);

      //@audit - accountOne and accountTwo both deposit 12ETH in the vault
      vm.prank(accountOne);
      savETHVault.depositETHForStaking{value: stakeAmount}(blsPubKeyOne, stakeAmount);
      vm.prank(accountTwo);
      savETHVault.depositETHForStaking{value: stakeAmount}(blsPubKeyOne, stakeAmount);

      LPToken lp = savETHVault.lpTokenForKnot(blsPubKeyOne);

      assertEq(lp.balanceOf(accountOne), 12 ether);

      skip(3600); //@audit - going 1
Medium: Node runner whitelisting functionality is broken
Audit: 2022-11-stakehouse-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2022-11-stakehouse/blob/4b6828e9c807f2f7c569e6d721ca1289f7cf7112/contracts/liquid-staking/LiquidStakingManager.sol#L280


# Vulnerability details

## Description

isNodeRunnerWhitelisted manages whitelisting of nodeRunners. If whitelisting is enabled, this mapping is checked in \_isNodeRunnerValid:
```
function _isNodeRunnerValid(address _nodeRunner) internal view returns (bool) {
    require(_nodeRunner != address(0), "Zero address");
    if(enableWhitelisting) {
        require(isNodeRunnerWhitelisted[_nodeRunner] == true, "Invalid node runner");
    }
    return true;
}
```

The whitelist is updated using updateNodeRunnerWhitelistStatus() function: 

```
function updateNodeRunnerWhitelistStatus(address _nodeRunner, bool isWhitelisted) external onlyDAO {  
    require(_nodeRunner != address(0), "Zero address");
    require(isNodeRunnerWhitelisted[_nodeRunner] != isNodeRunnerWhitelisted[_nodeRunner], "Unnecessary update to same status");
    isNodeRunnerWhitelisted[_nodeRunner] = isWhitelisted;
    emit NodeRunnerWhitelistingStatusChanged(_nodeRunner, isWhitelisted);
}
```

The issue is that the requirement is wrong:
`require(isNodeRunnerWhitelisted[_nodeRunner] != isNodeRunnerWhitelisted[_nodeRunner], "Unnecessary update to same status");`

It will never be satisfied. It was intended to be 
`require(isNodeRunnerWhitelisted[_nodeRunner] != isWhitelisted, "Unnecessary update to same status");`

## Impact

The entire whitelist functionality is broken and cannot be used.

## Tools Used

Manual audit

## Recommended Mitigation Steps

Change the faulty line to: 
`require(isNodeRunnerWhitelisted[_nodeRunner] != isWhitelisted, "Unnecessary update to same status");`
Slashing can prevent Protected Staking Pool depositors from redeeming their `ETH`
Audit: 2022-11-stakehouse-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2022-11-stakehouse/blob/4b6828e9c807f2f7c569e6d721ca1289f7cf7112/contracts/liquid-staking/SavETHVault.sol#L16


# Vulnerability details

The documentation states that:

```
The Protected Staking Pool is free from any slashing and leaking risk.
```

While the penalties affect the node operator first, then the Fees and MEV Pool stakers, if the slashing amount is higher, it will also impact the Protected Staking Pool.

As stated in [Ethereum's documentation](https://ethereum.org/en/developers/docs/consensus-mechanisms/pos/rewards-and-penalties/#slashing), the entirety of a validator's stake can be slashed:
```
The maximum slash is the full effective balance of all slashed validators (i.e. if there are lots of validators being slashed they could lose their entire stake)
```

In this case the Protected Pool would also incur losses, meaning either some depositors will not be able to retrieve their `ETH`  if `dETH/ETH` is not adjusted, or all depositors would take a loss from the `dETH/ETH` ratio being higher than 1.

## Impact

The slashing will always impact the the node operator first and the Fees and MEV Pool stakers first, meaning the Protected Staking Pool depositors are only grieved in cases where the slashing amount is large enough.

## Tools Used

Manual Analysis

## Mitigation

Users should be made aware that slashing can impact the protected pool too.
Single-step process for critical ownership transfer/renounce is risky
Audit: 2022-11-stakehouse-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2022-11-stakehouse/blob/23c3cf65975cada7fd2255a141b359a6b31c2f9c/contracts/syndicate/Syndicate.sol#L469-L488
https://github.com/code-423n4/2022-11-stakehouse/blob/23c3cf65975cada7fd2255a141b359a6b31c2f9c/contracts/smart-wallet/OwnableSmartWallet.sol#L94-L111


# Vulnerability details

# Single-step process for critical ownership transfer/renounce is risky
### Impact
The following contracts and functions, allow owners to interact with core functions such as:

- `execute`, `rawExecute` and `setApproval` in `OwnableSmartWallet`
- `registerKnotsToSyndicate`, `deRegisterKnots`, `addPriorityStakers` and `updatePriorityStakingBlock` in `Syndicate`

Given that `Syndicate` and `OwnableSmartWallet` are derived from `Ownable`, the ownership management of this contract defaults to `Ownable` ’s `transferOwnership()` and `renounceOwnership()` methods which are not overridden in `Syndicate`, and it's overriden in `OwnableSmartWallet` but without needed checks and way of transfer. 

Such critical address transfer/renouncing in one-step is very risky because it is irrecoverable from any mistakes

Scenario: If an incorrect address, e.g. for which the private key is not known, is used accidentally then it prevents the use of all the `onlyOwner()` functions forever, which includes the changing of various critical addresses and parameters. This use of incorrect address may not even be immediately apparent given that these functions are probably not used immediately. 

When noticed, due to a failing `onlyOwner()` function call, it will force the redeployment of these contracts and require appropriate changes and notifications for switching from the old to new address. This will diminish trust in the protocol and incur a significant reputational damage.

### Github Permalinks
- `_transferOwnership` not checking also 0 address, can lead to losing ownership in a more error prone way
https://github.com/code-423n4/2022-11-stakehouse/blob/23c3cf65975cada7fd2255a141b359a6b31c2f9c/contracts/syndicate/Syndicate.sol#L469-L488

- in the overrided version
https://github.com/code-423n4/2022-11-stakehouse/blob/23c3cf65975cada7fd2255a141b359a6b31c2f9c/contracts/smart-wallet/OwnableSmartWallet.sol#L94-L111

### Proof of Concept
See similar High Risk severity finding from Trail-of-Bits Audit of Hermez.
https://github.com/trailofbits/publications/blob/master/reviews/hermez.pdf
See similar Medium Risk severity finding from Trail-of-Bits Audit of Uniswap V3:
https://github.com/Uniswap/v3-core/blob/main/audits/tob/audit.pdf

### Recommended steps
Recommend overriding the inherited methods to null functions and use separate functions for a two-step address change:
1. Approve a new address as a `pendingOwner`
2. A transaction from the `pendingOwner` address claims the pending ownership change.

This mitigates risk because if an incorrect address is used in step (1) then it can be fixed by re-approving the correct address. Only after a correct a
Medium: Vaults can be griefed to not be able to be used for deposits
Audit: 2022-11-stakehouse-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2022-11-stakehouse/blob/4b6828e9c807f2f7c569e6d721ca1289f7cf7112/contracts/liquid-staking/ETHPoolLPFactory.sol#L111


# Vulnerability details

## Description

Interaction with SavETHVault and StakingFundsVault require a minimum amount of MIN_STAKING_AMOUNT. In order to be used for staking, there needs to be 24 ETH or 4 ETH for the desired BLS public key in those vaults. The issue is that vaults can be griefed and made impossible to use for depositing by constantly making sure the *remaining* amount to be added to complete the deposit to the maxStakingAmountPerValidator, is under MIN_STAKING_AMOUNT.

In \_depositETHForStaking:
```
function _depositETHForStaking(bytes calldata _blsPublicKeyOfKnot, uint256 _amount, bool _enableTransferHook) internal {
    require(_amount >= MIN_STAKING_AMOUNT, "Min amount not reached");
    require(_blsPublicKeyOfKnot.length == 48, "Invalid BLS public key");
    // LP token issued for the KNOT
    // will be zero for a new KNOT because the mapping doesn't exist
    LPToken lpToken = lpTokenForKnot[_blsPublicKeyOfKnot];
    if(address(lpToken) != address(0)) {
        // KNOT and it's LP token is already registered
        // mint the respective LP tokens for the user
        // total supply after minting the LP token must not exceed maximum staking amount per validator
        require(lpToken.totalSupply() + _amount <= maxStakingAmountPerValidator, "Amount exceeds the staking limit for the validator");
        // mint LP tokens for the depoistor with 1:1 ratio of LP tokens and ETH supplied
        lpToken.mint(msg.sender, _amount);
        emit LPTokenMinted(_blsPublicKeyOfKnot, address(lpToken), msg.sender, _amount);
    }
    else {
	
        // check that amount doesn't exceed max staking amount per validator
        require(_amount <= maxStakingAmountPerValidator, "Amount exceeds the staking limit for the validator");
    ...    

```


MED - Can grief vaults (SavETHVault, StakingFundsVault) and make them not able to be used for staking by depositing so that left to stake is < MIN_STAKING_AMOUNT. Then it will fail maxStakingAmount check @ _depositEthForStaking

## Impact

Vaults can be griefed to not be able to be used for deposits

## Proof of Concept

1. savETHVault has 22 ETH for some validator
2. Attacker deposits 1.9991 ETH to the savETHVault
3. vault now has 23.9991 ETH. The remaining to complete to 24 is 0.0009 ETH which is under 0.001 ether, min staking amount
4. No one can complete the staking

Note that depositers may try to remove their ETH and redeposit it to complete the deposit to 24. However attack may still keep the delta just under MIN_STAKING_AMOUNT.

## Tools Used

Manual audit

## Recommended Mitigation Steps

Handle the case where the remaining amount to be completed is smaller than MIN_STAKING_AMOUNT, and allow the deposit in that case.
Medium: Users receive an incorrect account of their accumulated ETH in GiantMevAndFeesPool.
Audit: 2022-11-stakehouse-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2022-11-stakehouse/blob/4b6828e9c807f2f7c569e6d721ca1289f7cf7112/contracts/liquid-staking/GiantMevAndFeesPool.sol#L91


# Vulnerability details

## Description

GiantMEVAndFeesPool exposes previewAccumulatedETH for users to view how much ETH they have accumulated through the vault:

```
function previewAccumulatedETH(
    address _user,
    address[] calldata _stakingFundsVaults,
    LPToken[][] calldata _lpTokens
) external view returns (uint256) {
    require(_stakingFundsVaults.length == _lpTokens.length, "Inconsistent array lengths");
    uint256 accumulated;
    for (uint256 i; i < _stakingFundsVaults.length; ++i) {
        accumulated = StakingFundsVault(payable(_stakingFundsVaults[i])).batchPreviewAccumulatedETH(
            address(this),
            _lpTokens[i]
        );
    }
    return _previewAccumulatedETH(_user, address(lpTokenETH), lpTokenETH.balanceOf(_user), lpTokenETH.totalSupply(), accumulated);
}
```

The issue is that accumulated variable is *overridden* instead of added to. This means only the last stakingFundVault's ETH will be taken into account when calculating the preview.

## Impact

Users receive an incorrect account of their accumulated ETH in GiantMevAndFeesPool. This may cause them to make an incorrect financial decision.

## Tools Used

Manual audit

## Recommended Mitigation Steps

Use += instead of = when calculating accumulated value.
Use of `transfer()` instead of `call()` to send eth
Audit: 2022-11-stakehouse-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2022-11-stakehouse/blob/23c3cf65975cada7fd2255a141b359a6b31c2f9c/contracts/syndicate/Syndicate.sol#L275
https://github.com/code-423n4/2022-11-stakehouse/blob/23c3cf65975cada7fd2255a141b359a6b31c2f9c/contracts/liquid-staking/GiantMevAndFeesPool.sol#L184
https://github.com/code-423n4/2022-11-stakehouse/blob/23c3cf65975cada7fd2255a141b359a6b31c2f9c/contracts/liquid-staking/GiantSavETHVaultPool.sol#L108
https://github.com/code-423n4/2022-11-stakehouse/blob/23c3cf65975cada7fd2255a141b359a6b31c2f9c/contracts/liquid-staking/GiantPoolBase.sol#L86


# Vulnerability details


# Use of `transfer()` instead of `call()` to send eth
## Impact
Use of `transfer()` might render ETH impossible to withdraw because after istanbul hardfork, there is increases in the gas cost of the `SLOAD` operation and therefore breaks some existing smart contracts. Those contracts will break because their fallback functions used to consume less than `2300` gas, and they’ll now consume more, since `2300` the amount of gas a contract’s fallback function receives if it’s called via Solidity’s `transfer()` or `send()` methods.

Any smart contract that uses `transfer()` or `send()` is taking a hard dependency on gas costs by forwarding a fixed amount of gas: `2300`.

### References
https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/
https://blog.openzeppelin.com/opyn-gamma-protocol-audit/

## Proof of Concept


https://github.com/code-423n4/2022-11-stakehouse/blob/23c3cf65975cada7fd2255a141b359a6b31c2f9c/contracts/liquid-staking/GiantPoolBase.sol#L86
            token.transfer(msg.sender, amount);

https://github.com/code-423n4/2022-11-stakehouse/blob/23c3cf65975cada7fd2255a141b359a6b31c2f9c/contracts/liquid-staking/GiantSavETHVaultPool.sol#L108
        getDETH().transfer(msg.sender, dETHReceivedFromAllSavETHVaults);

https://github.com/code-423n4/2022-11-stakehouse/blob/23c3cf65975cada7fd2255a141b359a6b31c2f9c/contracts/liquid-staking/GiantMevAndFeesPool.sol#L184
            _lpTokens[i].transfer(address(this), _lpTokens[i].balanceOf(address(this)));

https://github.com/code-423n4/2022-11-stakehouse/blob/23c3cf65975cada7fd2255a141b359a6b31c2f9c/contracts/syndicate/Syndicate.sol#L275
            bool transferResult = sETH.transfer(_sETHRecipient, _sETHAmount);



### Recommended Mitigation Steps
Use `call()` to send eth
Medium:  Giant pools are prone to user griefing, preventing their holdings from being staked.
Audit: 2022-11-stakehouse-findings
Severity: MEDIUM

# Lines of code

 https://github.com/code-423n4/2022-11-stakehouse/blob/4b6828e9c807f2f7c569e6d721ca1289f7cf7112/contracts/liquid-staking/GiantMevAndFeesPool.sol#L105


# Vulnerability details

## Description

batchRotateLPTokens in GiantMevAndFeesPool allows any user to rotate LP tokens of stakingFundsVaults around.

```
function batchRotateLPTokens(
    address[] calldata _stakingFundsVaults,
    LPToken[][] calldata _oldLPTokens,
    LPToken[][] calldata _newLPTokens,
    uint256[][] calldata _amounts
) external {
    uint256 numOfRotations = _stakingFundsVaults.length;
    require(numOfRotations > 0, "Empty arrays");
    require(numOfRotations == _oldLPTokens.length, "Inconsistent arrays");
    require(numOfRotations == _newLPTokens.length, "Inconsistent arrays");
    require(numOfRotations == _amounts.length, "Inconsistent arrays");
    require(lpTokenETH.balanceOf(msg.sender) >= 0.5 ether, "No common interest");
    for (uint256 i; i < numOfRotations; ++i) {
        StakingFundsVault(payable(_stakingFundsVaults[i])).batchRotateLPTokens(_oldLPTokens[i], _newLPTokens[i], _amounts[i]);
    }
}
```

There is a check that sender has over 0.5 ether of lpTokenETH, to prevent griefing. However, this check is unsatisfactory as user can at any stage deposit ETH to receive lpTokenETH and burn it to receive back ETH. Their lpTokenETH holdings do not correlate with their interest in the vault funds.

Therefore, malicious users can keep bouncing LP tokens around and prevent them from being available for actual staking by liquid staking manager.

## Impact

Giant pools are prone to user griefing, preventing their holdings from being staked.

## Tools Used

Manual audit

## Recommended Mitigation Steps

Three options:
1. batchRotateLPTokens should have logic to enforce that this specific rotation is logical
2. only DAO or some priviledged user can perform Giant pool operations
3. Make the caller have something to lose from behaving maliciously, unlike the current status.
Med: withdrawDETH is not functional for array lengths greater than one.
Audit: 2022-11-stakehouse-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2022-11-stakehouse/blob/4b6828e9c807f2f7c569e6d721ca1289f7cf7112/contracts/liquid-staking/GiantSavETHVaultPool.sol#L87


# Vulnerability details

## Description

The withdrawDETH() function is used in GiantSavETHVaultPool to burn user's LP  tokens and grant them dETH.
It loops over all input vaults and all input LPTokens, and for each one calls `lpTokenETH.burn(msg.sender, amount);`
Before that, it uses `_assertUserHasEnoughGiantLPToClaimVaultLP`   check:

```
function _assertUserHasEnoughGiantLPToClaimVaultLP(LPToken _token, uint256 _amount) internal view {
    require(_amount >= MIN_STAKING_AMOUNT, "Invalid amount");
    require(_token.balanceOf(address(this)) >= _amount, "Pool does not own specified LP");
    require(lpTokenETH.lastInteractedTimestamp(msg.sender) + 1 days < block.timestamp, "Too new");
}
```

It turns out this function will never be able to work unless user specifies only 1 LP token and 1 Vault. In any other case, this check will revert `lpTokenETH.lastInteractedTimestamp(msg.sender) + 1 days < block.timestamp` .

lpTokenETH.burn() does:
```
function burn(address _recipient, uint256 _amount) external {
    require(msg.sender == pool, "Only pool");
    _burn(_recipient, _amount);
}
```

\_burn calls transfer hooks:
```
function _burn(address account, uint256 amount) internal virtual {
    require(account != address(0), "ERC20: burn from the zero address");
    _beforeTokenTransfer(account, address(0), amount);
    uint256 accountBalance = _balances[account];
    require(accountBalance >= amount, "ERC20: burn amount exceeds balance");
    unchecked {
        _balances[account] = accountBalance - amount;
    }
    _totalSupply -= amount;
    emit Transfer(account, address(0), amount);
    _afterTokenTransfer(account, address(0), amount);
}
```

The afterTokenTransferHook updates the timestamp of sender and receiver of GiantLP token:
```
function _afterTokenTransfer(address _from, address _to, uint256 _amount) internal override {
    lastInteractedTimestamp[_from] = block.timestamp;
    lastInteractedTimestamp[_to] = block.timestamp;
    if (address(transferHookProcessor) != address(0)) ITransferHookProcessor(transferHookProcessor).afterTokenTransfer(_from, _to, _amount);
}
```

The end result is that after 1 transfer, the lastInteractedTimestamp of msg.sender will be current timestamp, and then it will be checked in the next iteration of withdrawDETH, which will assert that this value is greater than 1 day ago. That will cause revert.

## Impact

withdrawDETH is not functional for array lengths greater than one.

## Tools Used

Manual audit

## Recommended Mitigation Steps

Use a different accounting technique to check recent use of lpTokenETH, so that withdrawals are allowed.