xanoutas/solidity-security-findings huggingface dataset:
SLOT owners lose half their value when there is no sETH deposited to the Syndicate.
Audit: 2022-11-stakehouse-findings
Severity: HIGH

# Lines of code

 https://github.com/code-423n4/2022-11-stakehouse/blob/4b6828e9c807f2f7c569e6d721ca1289f7cf7112/contracts/syndicate/Syndicate.sol#L191


# Vulnerability details

## Description

The Syndicate contract uses total balance as collateral for both sETH (accumulatedETHPerFreeFloatingShare) and SLOT rewards (accumulatedETHPerCollateralizedSlotPerKnot).

They are updated in the following function:
```
function updateAccruedETHPerShares() public {
    // Ensure there are registered KNOTs. Syndicates are deployed with at least 1 registered but this can fall to zero.
    // Fee recipient should be re-assigned in the event that happens as any further ETH can be collected by owner
    if (numberOfRegisteredKnots > 0) {
        // All time, total ETH that was earned per slot type (free floating or collateralized)
        uint256 totalEthPerSlotType = calculateETHForFreeFloatingOrCollateralizedHolders();
        // Process free floating if there are staked shares
        uint256 freeFloatingUnprocessed;
        if (totalFreeFloatingShares > 0) {
            freeFloatingUnprocessed = getUnprocessedETHForAllFreeFloatingSlot();
            accumulatedETHPerFreeFloatingShare += _calculateNewAccumulatedETHPerFreeFloatingShare(freeFloatingUnprocessed);
            lastSeenETHPerFreeFloating = totalEthPerSlotType;
        }
        uint256 collateralizedUnprocessed = ((totalEthPerSlotType - lastSeenETHPerCollateralizedSlotPerKnot) / numberOfRegisteredKnots);
        accumulatedETHPerCollateralizedSlotPerKnot += collateralizedUnprocessed;
        lastSeenETHPerCollateralizedSlotPerKnot = totalEthPerSlotType;
        emit UpdateAccruedETH(freeFloatingUnprocessed + collateralizedUnprocessed);
    } else {
        // todo - check else case for any ETH lost
    }
}
```

calculateETHForFreeFloatingOrCollateralizedHolders() divides the collateral evenly between sETH and SLOT: 

```
function calculateETHForFreeFloatingOrCollateralizedHolders() public view returns (uint256) { // TOTAL ETH / 2
    // Get total amount of ETH that can be drawn down by all SLOT holders associated with a knot
    uint256 ethPerKnot = totalETHReceived();
    // Get the amount of ETH eligible for free floating sETH or collateralized SLOT stakers
    return ethPerKnot / 2;
}
```

So, `totalEthPerSlotType` holds half of the total ETH for collateral.
When `totalFreeFloatingShares > 0`,  they are split fine between the two tokens. However, when  `totalFreeFloatingShares = 0` , Syndicate does not use half of the balance dedicated to floating shares, and it is wasted. The desired behavior is to update lastSeenETHPerCollateralizedSlotPerKnot with the entire ETH balance. Ultimately, the impact is that SLOT owners would get half of the rewards they are supposed to, making the staking lose half the value.

## Impact

SLOT owners lose half their value when there is no sETH deposited to the Syndicate (lose of yield).

## Tools Used

Manual audit

## Recommended Mitigation Steps

totalEthPerSlotT
HIGH : The Giant pools can be drained by any user.
Audit: 2022-11-stakehouse-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Description

In GiantSavETHVaultPool.sol, batchDepositETHForStaking is used to deposit held funds to savETHPool vault:

```
function batchDepositETHForStaking(
    address[] calldata _savETHVaults,
    uint256[] calldata _ETHTransactionAmounts,
    bytes[][] calldata _blsPublicKeys,
    uint256[][] calldata _stakeAmounts
) public {
    uint256 numOfSavETHVaults = _savETHVaults.length;
    require(numOfSavETHVaults > 0, "Empty arrays");
    require(numOfSavETHVaults == _ETHTransactionAmounts.length, "Inconsistent array lengths");
    require(numOfSavETHVaults == _blsPublicKeys.length, "Inconsistent array lengths");
    require(numOfSavETHVaults == _stakeAmounts.length, "Inconsistent array lengths");
    // For every vault specified, supply ETH for at least 1 BLS public key of a LSDN validator
    for (uint256 i; i < numOfSavETHVaults; ++i) {
        uint256 transactionAmount = _ETHTransactionAmounts[i];
        // As ETH is being deployed to a savETH pool vault, it is no longer idle
        idleETH -= transactionAmount;
        SavETHVault savETHPool = SavETHVault(_savETHVaults[i]);
        require(
            liquidStakingDerivativeFactory.isLiquidStakingManager(address(savETHPool.liquidStakingManager())),
            "Invalid liquid staking manager"
        );
        // Deposit ETH for staking of BLS key
        savETHPool.batchDepositETHForStaking{ value: transactionAmount }(
            _blsPublicKeys[i],
            _stakeAmounts[i]
        );
    }
}
```

The critical issue is that this function does not have any access control AND there is insufficient validation that the passed \_savETHVaults which receives the funds is legitimate:
```
SavETHVault savETHPool = SavETHVault(_savETHVaults[i]);
require(
    liquidStakingDerivativeFactory.isLiquidStakingManager(address(savETHPool.liquidStakingManager())),
    "Invalid liquid staking manager"
);
```

This check can be easily passed by creating a malicious contract which returns an existing, valid liquid staking manager in the liquidStakingManager() call. 
Because this check is passed, a malicious user can steal the entire amount stored in the Giant pool by specifying their own contract as savETHVault.

This issue exists in both Giant pools.

## Impact

The Giant pools can be drained by any user.

## Tools Used

Manual audit

## Recommended Mitigation Steps

Enforce the connection between the stakingFundsVault and the liquidStakingManager in both directions.
Unchecked transfer
Audit: 2022-11-stakehouse-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
The return value of an external transfer/transferFrom call is not checked

## Proof of Concept
https://github.com/crytic/slither/wiki/Detector-Documentation#unchecked-transfer

you can check Vulnerability in code here:
https://github.com/code-423n4/2022-11-stakehouse/blob/4b6828e9c807f2f7c569e6d721ca1289f7cf7112/contracts/liquid-staking/GiantMevAndFeesPool.sol#L184

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

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

## Tools Used

## Recommended Mitigation Steps
Use SafeERC20, or ensure that the transfer/transferFrom return value is checked.
potential reentrancy risk in `_distributeETHRewardsToUserForToken`
Audit: 2022-11-stakehouse-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

potential reentrancy risk in `_distributeETHRewardsToUserForToken`

## Impact
Reentrancy risk can potentially be demaging

## Proof of Concept
`_distributeETHRewardsToUserForToken`, which in turn calls [`(bool success, ) = _recipient.call{value: due}("");`](https://github.com/code-423n4/2022-11-stakehouse/blob/4b6828e9c807f2f7c569e6d721ca1289f7cf7112/contracts/liquid-staking/SyndicateRewardsProcessor.sol#L67). As recipient of ETH, the msg.sender can potentially reenter.
`_distributeETHRewardsToUserForToken` is being used in multiple places, such as 
[`batchDepositETHForStaking` in `SavETHVault`](https://github.com/code-423n4/2022-11-stakehouse/blob/4b6828e9c807f2f7c569e6d721ca1289f7cf7112/contracts/liquid-staking/SavETHVault.sol#L57)
[`beforeTokenTransfer` in `SavETHVault`](https://github.com/code-423n4/2022-11-stakehouse/blob/4b6828e9c807f2f7c569e6d721ca1289f7cf7112/contracts/liquid-staking/SavETHVault.sol#L146)
[`batchDepositETHForStaking` in `StakingFundsVault`](https://github.com/code-423n4/2022-11-stakehouse/blob/4b6828e9c807f2f7c569e6d721ca1289f7cf7112/contracts/liquid-staking/StakingFundsVault.sol#L69)
[`beforeTokenTransfer` in `StakingFundsVault`](https://github.com/code-423n4/2022-11-stakehouse/blob/4b6828e9c807f2f7c569e6d721ca1289f7cf7112/contracts/liquid-staking/StakingFundsVault.sol#L315)

## Tools Used
manual

## Recommended Mitigation Steps
Careful examination and consider adding `reentrancyGuard` to above functions.
Node operators cannot withdraw their ETH when they rage quit 
Audit: 2022-11-stakehouse-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

Node operators can opt for Rage Quit, after the BLS public key is staked.

In the current configuration, they will not be able to retrieve their staked `ETH`:

`withdrawETHForKnot()` will revert [here](https://github.com/code-423n4/2022-11-stakehouse/blob/4b6828e9c807f2f7c569e6d721ca1289f7cf7112/contracts/liquid-staking/LiquidStakingManager.sol#L335) if the lifecycle status is not `INITIALS_REGISTERED`.

The lifecycle status after rage quit being `EXITED`, the call will revert, meaning the node runner will have lost their `ETH`.

Note that this issue will only arise when withdrawals are enabled - after the Shanghai update.


## Impact

Medium

## Tools Used

Manual Analysis

## Mitigation

Change the lifecycle status check to include `EXITED`, so that after rage quit node runners can withdraw their ETH.
HIGH: ETH is forever locked in GiantMevAndFeesPool due to unaccepted burn()
Audit: 2022-11-stakehouse-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Description

GiantMevAndFeesPools and StakingFundsVault implement beforeTokenTransfer transfer hooks.

StakingFundsVault.sol:

```
function beforeTokenTransfer(address _from, address _to, uint256) external override {
    address syndicate = liquidStakingNetworkManager.syndicate();
    if (syndicate != address(0)) {
        LPToken token = LPToken(msg.sender);
        bytes memory blsPubKey = KnotAssociatedWithLPToken[token];
        require(blsPubKey.length > 0, "Invalid token");
        if (getAccountManager().blsPublicKeyToLifecycleStatus(blsPubKey) == IDataStructures.LifecycleStatus.TOKENS_MINTED) {
            // Claim any ETH for the BLS key mapped to this token
            bytes[] memory keys = new bytes[](1);
            keys[0] = blsPubKey;
            _claimFundsFromSyndicateForDistribution(syndicate, keys);
            // Update the accumulated ETH per minted derivative LP share
            updateAccumulatedETHPerLP();
            // distribute any due rewards for the `from` user
            if (_from != address(0)) {
                _distributeETHRewardsToUserForToken(_from, address(token), token.balanceOf(_from), _from);
            }
            // in case the new user has existing rewards - give it to them so that the after transfer hook does not wipe pending rewards
            _distributeETHRewardsToUserForToken(_to, address(token), token.balanceOf(_to), _to);
        }
    }
}
```

GiantMevAndFeesPool:
```
function beforeTokenTransfer(address _from, address _to, uint256) external {
    require(msg.sender == address(lpTokenETH), "Caller is not giant LP");
    updateAccumulatedETHPerLP();
    // Make sure that `_from` gets total accrued before transfer as post transferred anything owed will be wiped
    if (_from != address(0)) {
        _distributeETHRewardsToUserForToken(
            _from,
            address(lpTokenETH),
            lpTokenETH.balanceOf(_from),
            _from
        );
    }
    // Make sure that `_to` gets total accrued before transfer as post transferred anything owed will be wiped
    _distributeETHRewardsToUserForToken(
        _to,
        address(lpTokenETH),
        lpTokenETH.balanceOf(_to),
        _to
    );
}
```

Notice that they will always call \_distributeETHRewardsToUserForToken. It is implemented in base, SyndicateRewardsProcessor:
```
function _distributeETHRewardsToUserForToken(
    address _user,
    address _token,
    uint256 _balance,
    address _recipient
) internal {
    require(_recipient != address(0), "Zero address");
    uint256 balance = _balance;
    if (balance > 0) {
        // Calculate how much ETH rewards the address is owed / due 
        uint256 due = ((accumulatedETHPerLPShare * balance) / PRECISION) - claimed[_user][_token];
        if (due > 0) {
            claimed[_user][
Medium: Attacker may withdraw arbitrary amount from smart wallet, even if state checks would not normally allow it
Audit: 2022-11-stakehouse-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Description


withdrawETHForKnot in LiquidStakingManager suffers from reentrancy attack.

```
function withdrawETHForKnot(address _recipient, bytes calldata _blsPublicKeyOfKnot) external {
    require(_recipient != address(0), "Zero address");
    require(isBLSPublicKeyBanned(_blsPublicKeyOfKnot) == false, "BLS public key has already withdrawn or not a part of LSD network");
    address associatedSmartWallet = smartWalletOfKnot[_blsPublicKeyOfKnot];
    require(smartWalletOfNodeRunner[msg.sender] == associatedSmartWallet, "Not the node runner for the smart wallet ");
    require(isNodeRunnerBanned(nodeRunnerOfSmartWallet[associatedSmartWallet]) == false, "Node runner is banned from LSD network");
    require(associatedSmartWallet.balance >= 4 ether, "Insufficient balance");
    require(
        getAccountManager().blsPublicKeyToLifecycleStatus(_blsPublicKeyOfKnot) == IDataStructures.LifecycleStatus.INITIALS_REGISTERED,
        "Initials not registered"
    );
    // refund 4 ether from smart wallet to node runner's EOA
    IOwnableSmartWallet(associatedSmartWallet).rawExecute(
        _recipient,
        "",
        4 ether
    );
    // update the mapping
    bannedBLSPublicKeys[_blsPublicKeyOfKnot] = associatedSmartWallet;
    emit ETHWithdrawnFromSmartWallet(associatedSmartWallet, _blsPublicKeyOfKnot, msg.sender);
}
```

The issue is that the public key is checked to not be banned at the start:
`require(isBLSPublicKeyBanned(_blsPublicKeyOfKnot) == false`

Next, execution is given to the user here:
```
IOwnableSmartWallet(associatedSmartWallet).rawExecute(
            _recipient,
            "",
            4 ether
        );
```

Finally, the banned mapping is updated:
`        bannedBLSPublicKeys[_blsPublicKeyOfKnot] = associatedSmartWallet;`

Therefore, malicious user can withdraw all ETH from their smart wallet, even for ones that are banned or that are not in INITIALS_REGISTERS state. This means they can rug the wallet.

## Impact

Attacker may withdraw arbitrary amount from smart wallet, even if state checks would not normally allow it

## Tools Used

Manual audit

## Recommended Mitigation Steps

Add a reentrancy guard  to the function. Additionally, ban the public key before passing control to user code.
an early staker in `StakingFundsVault` can take other stakers `eth`
Audit: 2022-11-stakehouse-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2022-11-stakehouse/blob/main/contracts/liquid-staking/SyndicateRewardsProcessor.sol#L76-L90


# Vulnerability details

## Impact
The first stakers in a `StakingFundsVault` can intentionally or unintentionally take `eth`from other stakers. If they claim rewards while `eth` is in the contract but yet not used to stake towards a validator that `eth` can be counted as rewards.

## Proof of Concept
The issue comes from that the `StakingFundsVault` doesn't track `eth` sent by early stakers. `totalEthSeen` is only tracked after derivatives are first minted:

`liquid-staking/StakingFundsVault.sol`
```javascript
55:    /// @notice Allows the liquid staking manager to notify funds vault about new derivatives minted to enable MEV claiming
56:    function updateDerivativesMinted() external onlyManager {
57:        // We know 4 ETH for the KNOT came from this vault so increase the shares to get a % of vault rewards
58:        totalShares += 4 ether;
59:    }
60:
61:    /// @notice For knots that have minted derivatives, update accumulated ETH per LP
62:    function updateAccumulatedETHPerLP() public {
63:        _updateAccumulatedETHPerLP(totalShares);
64:    }
```

`liquid-staking/SyndicateRewardsProcessor.sol`:
```javascript
75:    /// @dev Internal logic for tracking accumulated ETH per share
76:    function _updateAccumulatedETHPerLP(uint256 _numOfShares) internal {
77:        if (_numOfShares > 0) {
78:            uint256 received = totalRewardsReceived();
79:            uint256 unprocessed = received - totalETHSeen;
80:
81:            if (unprocessed > 0) {
82:                emit ETHReceived(unprocessed);
83:
84:                // accumulated ETH per minted share is scaled to avoid precision loss. it is scaled down later
85:                accumulatedETHPerLPShare += (unprocessed * PRECISION) / _numOfShares;
86:
87:                totalETHSeen = received;
88:            }
89:        }
90:    }
```

Hence, early stakers `eth` is not tracked which makes `totalEthSeen` off by the amount of `eth` sent before first derivatives are minted.

PoC test in `StakingFundsVault.t.sol`:
```javascript
    function testRewardsReceiverCanStealOtherStakersEth() public {
        bytes memory anotherKey = fromHex("3aBdcff61a34eb6a034e343f20732456443a2ed6668ede04677adc1e15d2a24500a3e05cf7ad3dc3b2f3cc13fdc12fff");
        // register BLS key with the network
        registerSingleBLSPubKey(accountTwo, blsPubKeyFour, accountFive);
        registerSingleBLSPubKey(accountTwo, anotherKey, accountFive);

        // Do a deposit of 4 ETH for bls pub key four in the fees and mev pool
        maxETHDeposit(accountTwo, getBytesArrayFromBytes(blsPubKeyFour));

        // Do a deposit of 24 ETH for savETH pool
        liquidStakingManager.savETHVault().depositETHForStaking{value: 24 ether}(blsPubKeyFour, 24 ether);
        liquidStakingManager.savETHVault().depositETHForStaking{value: 24 ether}(anotherKey, 24 ether);

        stakeAndMint
withdrawETHForKnot is vulnerable to reentrancy attack
Audit: 2022-11-stakehouse-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2022-11-stakehouse/blob/main/contracts/liquid-staking/LiquidStakingManager.sol#L326
https://github.com/code-423n4/2022-11-stakehouse/blob/main/contracts/liquid-staking/LiquidStakingManager.sol#L340
https://github.com/code-423n4/2022-11-stakehouse/blob/main/contracts/smart-wallet/OwnableSmartWallet.sol#L63


# Vulnerability details

## Impact
The `withdrawETHForKnot` is vulnerable to reentrancy because the transfer is done before an important state change.

## Proof of Concept

```solidity
function withdrawETHForKnot(address _recipient, bytes calldata _blsPublicKeyOfKnot) external {
    require(_recipient != address(0), "Zero address");
    require(isBLSPublicKeyBanned(_blsPublicKeyOfKnot) == false, "BLS public key has already withdrawn or not a part of LSD network");

    address associatedSmartWallet = smartWalletOfKnot[_blsPublicKeyOfKnot];
    require(smartWalletOfNodeRunner[msg.sender] == associatedSmartWallet, "Not the node runner for the smart wallet ");
    require(isNodeRunnerBanned(nodeRunnerOfSmartWallet[associatedSmartWallet]) == false, "Node runner is banned from LSD network");
    require(associatedSmartWallet.balance >= 4 ether, "Insufficient balance");
    require(
        getAccountManager().blsPublicKeyToLifecycleStatus(_blsPublicKeyOfKnot) == IDataStructures.LifecycleStatus.INITIALS_REGISTERED,
        "Initials not registered"
    );

    // refund 4 ether from smart wallet to node runner's EOA
    IOwnableSmartWallet(associatedSmartWallet).rawExecute(
        _recipient,
        "",
        4 ether
    );

    // > We can reenter here, before updating the `bannedBLSPublicKeys` that would instead make this attack impossible because it's checked at the top: 
    // require(isBLSPublicKeyBanned(_blsPublicKeyOfKnot) == false, "BLS public key has already withdrawn or not a part of LSD network");
    // Create a reentrant contract that calls again this function in the fallback function, and put the attacker contract address as "_recipient" in this function

    // update the mapping
    bannedBLSPublicKeys[_blsPublicKeyOfKnot] = associatedSmartWallet;

    emit ETHWithdrawnFromSmartWallet(associatedSmartWallet, _blsPublicKeyOfKnot, msg.sender);
}
```

This attack allows us to empty the contract ETH balance with as much as 4 ether - 1 wei remaining because else the `rawExecute` would fail.

## Tools Used

Manual inspection

## Recommended Mitigation Steps

Put the state change before the external function call.

```solidity
// refund 4 ether from smart wallet to node runner's EOA
+ bannedBLSPublicKeys[_blsPublicKeyOfKnot] = associatedSmartWallet;

IOwnableSmartWallet(associatedSmartWallet).rawExecute(
    _recipient,
    "",
    4 ether
);

// update the mapping
// - bannedBLSPublicKeys[_blsPublicKeyOfKnot] = associatedSmartWallet;
```
`bringUnusedETHBackIntoGiantPool` in `GiantMevAndFeesPool` can be used to steal `LPTokens`
Audit: 2022-11-stakehouse-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
real `LPTokens` can be transferred out of `GiantMevAndFeesPool` through fake `_stakingFundsVaults` provided by an attacker.
https://github.com/code-423n4/2022-11-stakehouse/blob/4b6828e9c807f2f7c569e6d721ca1289f7cf7112/contracts/liquid-staking/GiantMevAndFeesPool.sol#L126

## Proof of Concept
`bringUnusedETHBackIntoGiantPool` takes in `_stakingFundsVaults`, `_oldLPTokens`, `_newLPTokens` and rotate `_amounts` from old to new tokens. The tokens are thoroughly verified by `burnLPForETH` in `ETHPoolLPFactory`. 
However, theres is no checking for the validity of `_stakingFundsVaults`, nor the relationship between `LPTokens` and `_stakingFundsVaults`. Therefore, an attacker can create fake contracts for `_stakingFundsVaults`, with `burnLPTokensForETH`, that takes `LPTokens` as parameters. The `msg.sender` in `burnLPTokensForETH` is `GiantMevAndFeesPool`, thus the attacker can transfer `LPTokens` that belongs to `GiantMevAndFeesPool` to any addresses it controls.

## Tools Used
manual

## Recommended Mitigation Steps
Always passing liquid staking manager address, checking its real and then requesting either the savETH vault or staking funds vault is a good idea to prove the validity of vaults.