xanoutas/solidity-security-findings huggingface dataset:
Unbounded loop can run out of gas in `withdraw` and `unstake` operations
Audit: 2022-11-stakehouse-findings
Severity: MEDIUM

# Lines of code

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/syndicate/Syndicate.sol#L275
https://github.com/code-423n4/2022-11-stakehouse/blob/23c3cf65975cada7fd2255a141b359a6b31c2f9c/contracts/liquid-staking/GiantPoolBase.sol#L86


# Vulnerability details


# Unbounded loop can run out of gas in `withdraw` and `unstake` operations
### Impact
There are no bounds on the number of orders in the loop, this can run out of gas due to cost of the operations that can vary by the time.
### Proof Of Concept
https://github.com/code-423n4/2022-11-stakehouse/blob/23c3cf65975cada7fd2255a141b359a6b31c2f9c/contracts/liquid-staking/GiantPoolBase.sol#L86
```
    function withdrawLPTokens(LPToken[] calldata _lpTokens, uint256[] calldata _amounts) external {
        uint256 amountOfTokens = _lpTokens.length;
        require(amountOfTokens > 0, "Empty arrays");
        require(amountOfTokens == _amounts.length, "Inconsistent array lengths");

        _onWithdraw(_lpTokens);

        for (uint256 i; i < amountOfTokens; ++i) {
            LPToken token = _lpTokens[i];
            uint256 amount = _amounts[i];

            _assertUserHasEnoughGiantLPToClaimVaultLP(token, amount);

            // Burn giant LP from user before sending them an LP token from this pool
            lpTokenETH.burn(msg.sender, amount);

            // Giant LP tokens in this pool are 1:1 exchangeable with external savETH vault LP
            token.transfer(msg.sender, amount);

            emit LPSwappedForVaultLP(address(token), msg.sender, amount);
        }
    }
```

https://github.com/code-423n4/2022-11-stakehouse/blob/23c3cf65975cada7fd2255a141b359a6b31c2f9c/contracts/liquid-staking/GiantMevAndFeesPool.sol#L184
```
    function _onWithdraw(LPToken[] calldata _lpTokens) internal override {
        // Use the transfer hook of LPToken to trigger the claiming accrued ETH
        for (uint256 i; i < _lpTokens.length; ++i) {
            _lpTokens[i].transfer(address(this), _lpTokens[i].balanceOf(address(this)));
        }

        _distributeETHRewardsToUserForToken(
            msg.sender,
            address(lpTokenETH),
            lpTokenETH.balanceOf(msg.sender),
            msg.sender
        );
    }
```


https://github.com/code-423n4/2022-11-stakehouse/blob/23c3cf65975cada7fd2255a141b359a6b31c2f9c/contracts/syndicate/Syndicate.sol#L275
```
    function unstake(
        address _unclaimedETHRecipient,
        address _sETHRecipient,
        bytes[] calldata _blsPubKeys,
        uint256[] calldata _sETHAmounts
    ) external nonReentrant {
        if (_blsPubKeys.length == 0) revert EmptyArray();
        if (_blsPubKeys.length != _sETHAmounts.length) revert InconsistentArrayLengths();
        if (_unclaimedETHRecipient == address(0)) revert ZeroAddress();
        if (_sETHRecip
Medium: DAO can drain all funds of all node runners immediately
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#L202


# Vulnerability details

## Description

The DAO can steal all funds of all node runners in the system, which means the system is heavily centralized. 
```
function executeAsSmartWallet(
    address _nodeRunner,
    address _to,
    bytes calldata _data,
    uint256 _value
) external payable onlyDAO {
    address smartWallet = smartWalletOfNodeRunner[_nodeRunner];
    require(smartWallet != address(0), "No wallet found");
    IOwnableSmartWallet(smartWallet).execute(
        _to,
        _data,
        _value
    );
}
```

This function allows immediate arbitrary access to all smartWallets.Historically centralization threats such as this are considered M severity because users should be warned of their existence.

## Impact

DAO can drain all funds of all node runners immediately

## Tools Used

Manual audit

## Recommended Mitigation Steps

It is sensible that DAO should have priviledges access to accounts, to make sure malicious behavior can be responded to. However, the current methodology allows the DAO to rug all users far too easily.

Implement a timelock mechanism inside LiquidStakingManager, so that node runners have time to safely withdraw their funds in the case of a rug pull.
Medium: User receives less rewards than they are eligible for if first passed BLS key is inactive
Audit: 2022-11-stakehouse-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Description

StakingFundsVault has the claimRewards() function to allow users to withdraw profits. 
```
function claimRewards(
    address _recipient,
    bytes[] calldata _blsPubKeys
) external nonReentrant {
    for (uint256 i; i < _blsPubKeys.length; ++i) {
        require(
            liquidStakingNetworkManager.isBLSPublicKeyBanned(_blsPubKeys[i]) == false,
            "Unknown BLS public key"
        );
        // Ensure that the BLS key has its derivatives minted
        require(
            getAccountManager().blsPublicKeyToLifecycleStatus(_blsPubKeys[i]) == IDataStructures.LifecycleStatus.TOKENS_MINTED,
            "Derivatives not minted"
        );
        if (i == 0 && !Syndicate(payable(liquidStakingNetworkManager.syndicate())).isNoLongerPartOfSyndicate(_blsPubKeys[i])) {
            // Withdraw any ETH accrued on free floating SLOT from syndicate to this contract
            // If a partial list of BLS keys that have free floating staked are supplied, then partial funds accrued will be fetched
            _claimFundsFromSyndicateForDistribution(
                liquidStakingNetworkManager.syndicate(),
                _blsPubKeys
            );
            // Distribute ETH per LP
            updateAccumulatedETHPerLP();
        }
        // If msg.sender has a balance for the LP token associated with the BLS key, then send them any accrued ETH
        LPToken token = lpTokenForKnot[_blsPubKeys[i]];
        require(address(token) != address(0), "Invalid BLS key");
        require(token.lastInteractedTimestamp(msg.sender) + 30 minutes < block.timestamp, "Last transfer too recent");
        _distributeETHRewardsToUserForToken(msg.sender, address(token), token.balanceOf(msg.sender), _recipient);
    }
}
```

The issue is that updateAccumulatedETHPerLP() is not guaranteed to be called, which means the ETH reward distribution in \_distribute would use stale value, and users will not receive as many rewards as they should.
`updateAccumulatedETHPerLP` is only called if the first BLS public key is part of the syndicate. However, for the other keys it makes no reason not to use the up to date accumulatedETHPerLPShare value.

## Impact

User receives less rewards than they are eligible for if first passed BLS key is inactive.

## Tools Used

Manual audit

## Recommended Mitigation Steps

Call updateAccumulatedETHPerLP() at the start of the function.
MED: Funds are not claimed from syndicate for valid BLS keys of first key is invalid (no longer part of syndicate).
Audit: 2022-11-stakehouse-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Description

claimRewards in StakingFundsVault.sol has this code:
```
if (i == 0 && !Syndicate(payable(liquidStakingNetworkManager.syndicate())).isNoLongerPartOfSyndicate(_blsPubKeys[i])) {
    // Withdraw any ETH accrued on free floating SLOT from syndicate to this contract
    // If a partial list of BLS keys that have free floating staked are supplied, then partial funds accrued will be fetched
    _claimFundsFromSyndicateForDistribution(
        liquidStakingNetworkManager.syndicate(),
        _blsPubKeys
    );
    // Distribute ETH per LP
    updateAccumulatedETHPerLP();
}
```

The issue is that if the first BLS public key is not part of the syndicate, then \_claimFundsFromSyndicateForDistribution will not be called, even on BLS keys that are eligible for syndicate rewards. This leads to reduced rewards for user.

This is different from a second bug which discusses the possibility of using a stale acculmulatedETHPerLP.

## Impact

Users will not receive rewards for claims of valid public keys if first passed key is not part of syndicate.

## Tools Used

Manual audit

## Recommended Mitigation Steps

Drop the i==0 requirement, which was intended to make sure the claim isn't called multiple times. Use a hasClaimed boolean instead.
Medium: Users may have lock funds of up to MIN_STAKING_AMOUNT in pools.
Audit: 2022-11-stakehouse-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Description

GiantPoolBase, SavETHVault and StakingFundsVault manage LP tokens. When user deposits ETH, they reset tokens, and they can be burnt to redeem ETH.
GiantPoolBase implements withdrawals with:
```
function withdrawETH(uint256 _amount) external nonReentrant {
    require(_amount >= MIN_STAKING_AMOUNT, "Invalid amount");
    require(lpTokenETH.balanceOf(msg.sender) >= _amount, "Invalid balance");
    require(idleETH >= _amount, "Come back later or withdraw less ETH");
    idleETH -= _amount;
    lpTokenETH.burn(msg.sender, _amount);
    (bool success,) = msg.sender.call{value: _amount}("");
    require(success, "Failed to transfer ETH");
    emit LPBurnedForETH(msg.sender, _amount);
}
```

Note that the withdrawal amount must be larger than MIN_STAKING_AMOUNT. The issue is that user may have locked value of amount < MIN_STAKING_AMOUNT stuck in the contract. In that case, they will not be able to withdraw it because of this limitation.

## Impact

Users may have lock funds of up to MIN_STAKING_AMOUNT in pools.

## Proof of Concept

1. User deposits 1 ETH and receives 1 LP token
2. User burns 0.5 LP, receives 0.5 ETH
3. User burns 0.4991 LP, receives 0.4991 ETH
4. User cannot access remaining 0.0009 ETH because it is under the MIN_STAKING_AMOUNT

## Tools Used

Manual audit

## Recommended Mitigation Steps

If user wishes to withdraw the remaining balance, do not require them to withdraw MIN_STAKING_AMOUNT.
no reentrancy guard on ` mint() ` function that has a callback 
Audit: 2022-11-stakehouse-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2022-11-stakehouse/blob/main/contracts/liquid-staking/GiantLP.sol#L29-L32


# Vulnerability details

## Impact
In ` /GiantLP.sol `  the ` mint() ` function calls _safeMint() which has a callback to the “to” address argument. Functions with callbacks should have reentrancy guards in place for protection against possible malicious actors both from inside and outside the protocol.

## Proof of Concept

```
    function mint(address _recipient, uint256 _amount) external {
        require(msg.sender == pool, "Only pool");
        _mint(_recipient, _amount);
    }

```
## Tools Used
Manual 

## Recommended Mitigation Steps

Add a reentrancy guard modifier on the ` mint() `function in /GiantLP.sol
DOS TO DAO WHEN WHITELISTING A NODERUNNER
Audit: 2022-11-stakehouse-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
The second require conditional statement of `updateNodeRunnerWhitelistStatus()` in `LiquidStakingManager.sol` checks whether or not the left bool variable is not equal to the right bool variable. However, this comparison check entails two identical bools that will always return false, making the statement readily revert whenever this specific function is called. This  will lead to denial of service to the `dao` of its duties to enable/disable whitelisting of a noderunner.

## Proof of Concept
Here is the affected function instance found.

[Manager.sol#L278-L284](https://github.com/code-423n4/2022-11-stakehouse/blob/main/contracts/liquid-staking/LiquidStakingManager.sol#L278-L284)

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

282:        isNodeRunnerWhitelisted[_nodeRunner] = isWhitelisted;
283:        emit NodeRunnerWhitelistingStatusChanged(_nodeRunner, isWhitelisted);
284:    }
```
Note that on line 280, `(isNodeRunnerWhitelisted[_nodeRunner] != isNodeRunnerWhitelisted[_nodeRunner]) == false`. 

## Recommended Mitigation Steps

It is recommended having line 280 refactored by replacing the right bool variable with the second function parameter:

```
    require(isNodeRunnerWhitelisted[_nodeRunner] != isWhitelisted], "Unnecessary update to same status");
```
Nobody can get whitelisted in LSD manager
Audit: 2022-11-stakehouse-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2022-11-stakehouse/blob/main/contracts/liquid-staking/LiquidStakingManager.sol#L278
https://github.com/code-423n4/2022-11-stakehouse/blob/main/contracts/liquid-staking/LiquidStakingManager.sol#L280
https://github.com/code-423n4/2022-11-stakehouse/blob/main/contracts/liquid-staking/LiquidStakingManager.sol#L687-L689
https://github.com/code-423n4/2022-11-stakehouse/blob/main/contracts/liquid-staking/LiquidStakingManager.sol#L267-L273


# Vulnerability details

## Impact
In the `updateNodeRunnerWhitelistStatus()` function, nobody can get whitelisted because of a wrong checking.
If the DAO `enableWhitelisting` functionality is enabled, then no new node runner will be able to register a new BLS key, this would partly freeze the system.


## Proof of Concept
```solidity
function setUp() public {
    vm.startPrank(accountFive); // this will mean it gets dETH initial supply
    factory = createMockLSDNFactory();
    vm.stopPrank();

    // Deploy 1 network and get default dependencies
    manager = deployNewLiquidStakingNetwork(
        factory,
        admin,
        true,
        "LSDN"
    );

    savETHVault = getSavETHVaultFromManager(manager);
    stakingFundsVault = getStakingFundsVaultFromManager(manager);

    // make 'admin' the 'DAO'
    vm.prank(address(factory));
    manager.updateDAOAddress(admin);
}

function testCannotWhitelist() public {
    // Set up users and ETH
    address nodeRunner = accountOne; vm.deal(nodeRunner, 4 ether);
    // Node runner is not yet whitelisted
    bool wl = manager.isNodeRunnerWhitelisted(nodeRunner);

    // Prank as DAO
    vm.startPrank(admin);

    // Check by banning
    vm.expectRevert(bytes("Unnecessary update to same status"));
    manager.updateNodeRunnerWhitelistStatus(nodeRunner, false);

    // Check with true ?
    vm.expectRevert(bytes("Unnecessary update to same status"));
    manager.updateNodeRunnerWhitelistStatus(nodeRunner, true);

    // Both revert
}
```

## Tools Used
Manual inspection

## Recommended Mitigation Steps
Use the `isWhitelisted` value to check against the `isNodeRunnerWhitelisted` instead.

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

    isNodeRunnerWhitelisted[_nodeRunner] = isWhitelisted;
    emit NodeRunnerWhitelistingStatusChanged(_nodeRunner, isWhitelisted);
}
```
Griefing attack on `lastInteractedTimestamp` value
Audit: 2022-11-stakehouse-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2022-11-stakehouse/blob/39a3a84615725b7b2ce296861352117793e4c853/contracts/liquid-staking/GiantLP.sol#L43
https://github.com/code-423n4/2022-11-stakehouse/blob/39a3a84615725b7b2ce296861352117793e4c853/contracts/liquid-staking/LPToken.sol#L66


# Vulnerability details

## Description

Suppose Alice calls any of the functions that use `lastInteractedTimestamp` value for some special checks. Bob (attacker) monitors the mempool and sees the Alice transaction. Then he calls `transfer` in `GiantLP`/`LPToken` token (depending on the function the Alice calls) before the Alices transaction. He can call `transfer` with `amount` parameter equal to zero. As result, Alice's transaction fails and she paid for its gas. Please note, that Bob can force his transaction to be executed before Alices using a higher gas price or even MEV.

## Impact

`rotateLPTokens`, `withdrawLPTokens`, `withdrawDETH`, `burnLPToken`, `burnLPForETH`, `claimRewards` are vulnerable to the griefing attack.

## Recommended Mitigation Steps

Do not update `lastInteractedTimestamp` value that corresponds to an account in case the transaction is not initiated by it.
Griefing by sending tiny amounts of LP tokens to an address
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#L66-L70


# Vulnerability details

## Impact

The LP tokens have a mechanism that prevent certain interactions if some LP tokens have been recently added to the calling address. This can be used by a malicious actor to grief an address by sending tiny amounts of LP tokens to that address, either in certain intervals or by watching the mempools for transactions from that address and frontrunning those.

The features that are affected are claiming rewards, burning LPs for Ether, rotating LP tokens, and withdrawing LP tokens. 

## Proof of Concept

The `LPToken._afterTokenTransfer` hook sets the `lastInteractedTimestamp` for both `_from` and `_to`:

```js
lastInteractedTimestamp[_from] = block.timestamp;
lastInteractedTimestamp[_to] = block.timestamp;
```

This `lastInteractedTimestamp` is used to restrict the execution of certain functions such as `SavEthVault.burnLPToken`:

```js
bool isStaleLiquidity = _lpToken.lastInteractedTimestamp(msg.sender) + 30 minutes < block.timestamp;
...
require(isStaleLiquidity, "Liquidity is still fresh");
```

This allows transfers of tiny amounts to an address to prevent that address from calling functions that use the `lastInteractedTimestamp` in require statements. 

## Tools Used

Manual Review

## Recommended Mitigation Steps

A workaround could be by involving a checkpointing mechanism that stores balances for a blocknumber whenever the balance changes. However that would increase implementation complexity by a fair margin.
rotateNodeRunnerOfSmartWallet is vulnerable to a frontrun attack
Audit: 2022-11-stakehouse-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2022-11-stakehouse/blob/main/contracts/liquid-staking/LiquidStakingManager.sol#L356
https://github.com/code-423n4/2022-11-stakehouse/blob/main/contracts/liquid-staking/LiquidStakingManager.sol#L369


# Vulnerability details

## Impact
As the `rotateNodeRunnerOfSmartWallet` function can be called by anyone who is a node runner in the LSD network, this function is vulnerable to a frontrun attack in the case of this node runner being malicious.

## Proof of Concept
If that is the current node runner is malicious, the DAO would purposely call this same `rotateNodeRunnerOfSmartWallet` with the `_wasPreviousNodeRunnerMalicious` flag turned on.
An actual node runner that has been malicious could monitor the mempool and frontrun the DAO transaction that wanted to slash it and submit the transaction before the DAO to avoid getting banned and rotate their EOA representation of the node.

```solidity
if (msg.sender == dao && _wasPreviousNodeRunnerMalicious) {
    bannedNodeRunners[_current] = true;
    emit NodeRunnerBanned(_current);
}
```

When the DAO transaction would go through, it would revert when it's checking if the current (old) node representative is still a wallet, but it's not because the mapping value has been deleted before.

```solidity
address wallet = smartWalletOfNodeRunner[_current];
require(wallet != address(0), "Wallet does not exist");
```

## Tools Used
Manual inspection

## Recommended Mitigation Steps
Restrict this function to DAO only with the `onlyDAO` modifier.

```solidity
// - function rotateNodeRunnerOfSmartWallet(address _current, address _new, bool _wasPreviousNodeRunnerMalicious) external {
+ function rotateNodeRunnerOfSmartWallet(address _current, address _new, bool _wasPreviousNodeRunnerMalicious) onlyDAO external {
    require(_new != address(0) && _current != _new, "New is zero or current");

    address wallet = smartWalletOfNodeRunner[_current];
    require(wallet != address(0), "Wallet does not exist");
    require(_current == msg.sender || dao == msg.sender, "Not current owner or DAO");

    address newRunnerCurrentWallet = smartWalletOfNodeRunner[_new];
    require(newRunnerCurrentWallet == address(0), "New runner has a wallet");

    smartWalletOfNodeRunner[_new] = wallet;
    nodeRunnerOfSmartWallet[wallet] = _new;

    delete smartWalletOfNodeRunner[_current];

    // - if (msg.sender == dao && _wasPreviousNodeRunnerMalicious) {
    if (_wasPreviousNodeRunnerMalicious) {
        bannedNodeRunners[_current] = true;
        emit NodeRunnerBanned(_current);
    }

    emit NodeRunnerOfSmartWalletRotated(wallet, _current, _new);
}