xanoutas/solidity-security-findings huggingface dataset:
Changing the slashingHandler for NativeVaults will DoS slashing
Audit: 2024-07-karak-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-karak/blob/f5e52fdcb4c20c4318d532a9f08f7876e9afb321/src/NativeVault.sol#L308


# Vulnerability details

Relevant code: [NativeVault::slashAssets](https://github.com/code-423n4/2024-07-karak/blob/f5e52fdcb4c20c4318d532a9f08f7876e9afb321/src/NativeVault.sol#L308)
### Description
`Core`'s state contains a mapping between assets and slashingHandlers - `assetSlashingHandlers`. Assets that are assigned a slashing handler are considered whitelisted and vaults containing this asset can be deployed. 

When a slash is performed on a vault, the following function is called:
```js
function slashAssets(
	uint256 slashPercentageWad, address slashingHandler
) external returns (uint256 transferAmount);
```

Inside `NativeVault`'s implementation of the function, the following check is performed:
```js
if (slashingHandler != self.slashStore) revert NotSlashStore();
```

This check can be problematic in the case where some number of native vaults have been deployed and the `assetSlashingHandlers` mapping gets updated with a new `slashHandler`. Then, the `slashAssets` function will always revert.

### Root Cause
Unsynchronized state update causes DoS of slashing functionality inside `NativeVault`.

### Impact
If the `slashingHandler` for restaking gets changed at some point it will cause DoS of the slashing functionality for `NativeVaults`. It will be blocked until contracts get updated with new implementation or old slashing handler is restored. 

> The issue could be exacerbated if some new `NativeVaults` get deployed with the new slash handler. Then, reverting to the old slash handler will break the newly deployed `NativeVaults`, making update implementation the only option to fix this issue. But this is also troublesome because only normal `Vaults` can have they're implementation updated for all deployments with one call. `NativeVaults` would have to be updated one by one.

### PoC
Lets suppose the following scenario:
1. SlashHandler `A` is set for key `Constants.DEAD_BEEF` in `assetSlashingHandlers`.
2. 5 `NativeVaults` get deployed with this slash handler.
3. SlashHandler `B` is set for `Constants.DEAD_BEEF`.
4. These 5 `NativeVaults` would not be slashable until some measures are taken - either `A` restored or the vault's implementations are updated.

Both approaches to fix this issue once it has occurred have their drawbacks:
- If after **step 3**, more `NativeVaults` are created updating the allowlist will always cause some group of native vaults to be **unslashable**. 
- Changing the `NativeVault` implementations can only be done only 1 by 1, since the default vault implementation is for normal `Vault`.

### Suggested Mitigation
There are multiple ways to prevent this issue:
- Function only callable by `Manager` to change `slashStore`
- Function in `Core` that is callable by anyone which changes the underlying `slashStore` of a `NativeVault` if there is a difference. Allowing users to unDoS themselves.
A user can lose a significative amount of assets because of a lack of slippage protection 
Audit: 2024-07-karak-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-karak/blob/main/src/Vault.sol#L125-L149
https://github.com/code-423n4/2024-07-karak/blob/main/src/NativeVault.sol#L228-L258


# Vulnerability details

## Impact
The user can lose a significative part of his assets. 
## Proof of Concept

We can see here that the function startRedeem (line 125 in Vault.sol) has no slippage protection.

````
function startRedeem(uint256 shares, address beneficiary)
        external
        whenFunctionNotPaused(Constants.PAUSE_VAULT_START_REDEEM)
        nonReentrant
        returns (bytes32 withdrawalKey)
````
Moreover, a user must wait 9 days to call finishRedeem (line 157) as specified in the Constants library.
````
   uint256 public constant SLASHING_WINDOW = 7 days;
    uint256 public constant SLASHING_VETO_WINDOW = 2 days;
    uint256 public constant MIN_STAKE_UPDATE_DELAY = SLASHING_WINDOW + SLASHING_VETO_WINDOW;
    uint256 public constant MIN_WITHDRAWAL_DELAY = SLASHING_WINDOW + SLASHING_VETO_WINDOW;
````
During those 9 days, the price of the shares could drop significantly, leaving the user with substantially fewer assets.

## Tools Used
Unit test

## Recommended Mitigation Steps
Add a minOut parameter to the startRedeem function, revert finishRedeem if the assets out are less than this minimum, and store it in the QueuedWithdrawal struct. 
````
 function startRedeem(uint256 shares, address beneficiary, uint256 minOut)
        external
        whenFunctionNotPaused(Constants.PAUSE_VAULT_START_REDEEM)
        nonReentrant
        returns (bytes32 withdrawalKey)
    {
        if (shares == 0) revert ZeroShares();
        if (beneficiary == address(0)) revert ZeroAddress();

        (VaultLib.State storage state, VaultLib.Config storage config) = _storage();
        address staker = msg.sender;

        uint256 assets = convertToAssets(shares);

        withdrawalKey = WithdrawLib.calculateWithdrawKey(staker, state.stakerToWithdrawNonce[staker]++);

        state.withdrawalMap[withdrawalKey].staker = staker;
        state.withdrawalMap[withdrawalKey].start = uint96(block.timestamp);
        state.withdrawalMap[withdrawalKey].shares = shares;
        state.withdrawalMap[withdrawalKey].beneficiary = beneficiary;
        state.withdrawalMap[withdrawalKey].minAssetsOut =minOut;
        this.transferFrom(msg.sender, address(this), shares);

        emit StartedRedeem(staker, config.operator, shares, withdrawalKey, assets);
    }
````
````
library WithdrawLib {
    struct QueuedWithdrawal {
        address staker;
        uint96 start;
        uint256 shares;
        address beneficiary;
        uint256 minAssetsOut; 
    }
...WithdrawLib code
````



## Assessed type

Math
Failure to deploy a node due to an existing account at expected address
Audit: 2024-07-karak-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-karak/blob/main/src/entities/NativeVaultLib.sol#L93-L104


# Vulnerability details

## Impact
Failure of deploying a node and the system gets stuck.

## Proof of Concept
If there is an EOA or a contract at the expected address, the system may get stuck.
Node is deployed in a deterministic way at the the expected address which is generated by salt:
```
    function deployNode(Storage storage self, VaultLib.Config storage config, address owner)
        internal
        returns (address)
    {
        bytes32 salt = keccak256(abi.encodePacked(config.operator, owner));

        INativeNode newNode = INativeNode(address(LibClone.deployDeterministicERC1967BeaconProxy(address(this), salt)));
        newNode.initialize(address(this), owner);

        emit NodeDeployed(msg.sender, address(newNode), self.nodeImpl);
        return address(newNode);
    }
```
If there is an EOA or a contract at the expected address, then the native vault would keep failing to deploy a node for the `owner`.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Use the nonce to generate vault like in creating a vault by core.


## Assessed type

Invalid Validation
Failure to create a vault due to an existing account at expected address
Audit: 2024-07-karak-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-karak/blob/main/src/entities/CoreLib.sol#L99-L112


# Vulnerability details

## Impact
Failure of providing a vault creation to operators and the system gets stuck.

## Proof of Concept
If there is an EOA or a contract at the `expectedNewVaultAddr`, the system may get stuck.
Vault is created in a deterministic way at the `expectedNewVaultAddr` which is generated by salt:

```solidity
        bytes32 salt = keccak256(abi.encodePacked(operator, depositToken, self.vaultNonce++));

        address expectedNewVaultAddr =
            LibClone.predictDeterministicAddressERC1967BeaconProxy(address(this), salt, address(this));

        self.vaultToImplMap[address(expectedNewVaultAddr)] = implementation;

        IKarakBaseVault vault = cloneVault(salt);
        vault.initialize(address(this), operator, depositToken, name, symbol, extraData);

        // Extra protection to ensure the vault was created with the correct address
        if (expectedNewVaultAddr != address(vault)) {
            revert VaultCreationFailedAddrMismatch(expectedNewVaultAddr, address(vault));
        }
```

Assume at the moment, the `operator` is the only one trying to create a vault with his `depositToken` and there is an EOA or a contract at the `expectedNewVaultAddr`. Then the core would keep failing to create a vault and stuck until a fresh new request (with different operator or depositToken) comes in.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Increase the `self.vaultNonce` in such cases.


## Assessed type

Invalid Validation
An attacker can impose financial losses on other stakers by validating their validators prior to executing a slash.
Audit: 2024-07-karak-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-karak/blob/main/src/NativeVault.sol#L168-L204
https://github.com/code-423n4/2024-07-karak/blob/main/src/NativeVault.sol#L497-L505
https://github.com/code-423n4/2024-07-karak/blob/main/src/NativeVault.sol#L299-L318


# Vulnerability details

## Impact

An attacker can impose financial losses on other stakers by validating their validators prior to executing a slash.

## Proof of Concept

The `validateWithdrawalCredentials()` function lacks a caller check, allowing anyone to invoke it and validate other stakers' validators. This vulnerability can be exploited by attackers to cause financial losses to other stakers.

```
    function validateWithdrawalCredentials(
        address nodeOwner,
        BeaconProofs.BeaconStateRootProof calldata beaconStateRootProof,
        BeaconProofs.ValidatorFieldsProof[] calldata validatorFieldsProofs
    )
        external
        nonReentrant
        nodeExists(nodeOwner)
        whenFunctionNotPaused(Constants.PAUSE_NATIVEVAULT_VALIDATE_WITHDRAW_CREDS)
    {
        NativeVaultLib.Storage storage self = _state();
        NativeVaultLib.NativeNode storage node = self.ownerToNode[nodeOwner];

        if (beaconStateRootProof.timestamp == block.timestamp) {
            revert BeaconTimestampIsCurrent();
        }
        if (
            beaconStateRootProof.timestamp < node.lastSnapshotTimestamp
                || beaconStateRootProof.timestamp < node.currentSnapshotTimestamp
        ) revert BeaconTimestampTooOld();

        uint256 totalRestakedWei;
        BeaconProofs.validateBeaconStateRootProof(
            _getParentBlockRoot(beaconStateRootProof.timestamp), beaconStateRootProof
        );

        for (uint256 i = 0; i < validatorFieldsProofs.length; i++) {
            totalRestakedWei += self.validateWithdrawalCredentials(
                nodeOwner,
                beaconStateRootProof.timestamp,
                beaconStateRootProof.beaconStateRoot,
                validatorFieldsProofs[i]
            );
        }

203     _increaseBalance(nodeOwner, totalRestakedWei);
    }
```

Consider the following scenario:

1. Current state of the `NativeVault`:
    - `totalAssets`: 64 ETH (Alice: 32 ETH, Bob: 32 ETH)
    - `totalSupply`: s + s = 2s (Alice's balance is s, and Bob's balance is s, too.)
2. Alice has an unvalidated validator with 32 ETH and there is a pending slashing of 3 ETH, which means Alice and Bob are supposed to be slashed equally by 1.5 ETH each.
3. Bob, a malicious staker, calls the `validateWithdrawalCredentials()` function to validate Alice's validator. This alters the state during the `_increaseBalance()` function call `L203`:
    ```solidity
        function _increaseBalance(address _of, uint256 assets) internal {
            ...

            _mint(_of, shares);
            self.totalAssets += assets;
            ...
    ```
    Updated state:
    - totalAssets: 96 ETH (Alice: 64 ETH, Bob: 32 ETH)
    - totalSupply: 3s (Alice: 2s, Bob:
A risk of losing funds exists when depositing just before slashing.
Audit: 2024-07-karak-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-karak/blob/main/src/Vault.sol#L193-L205


# Vulnerability details

## Impact

Stakers may lose a portion of their deposit if the deposit is made just before slashing.

## Proof of Concept

Let's consider a simplified scenario:

1. There are 3 pending slashing requests for the vault:
    - Each slashing amount is `$50`.
    - The total assets of the vault are `$125`.
2. The 1st and 2nd requests are finalized:
    - The total assets decrease to `$125 - ($50 * 2) = $25`.
    - One pending slashing request remains.

At this point, the last slashing will only partially slash the remaining `$25` of the vault, as seen below.

```solidity
    function slashAssets(uint256 totalAssetsToSlash, address slashingHandler)
        ...
198     transferAmount = Math.min(totalAssets(), totalAssetsToSlash);
        ...
```

3. Before the final slashing occurs, a staker deposits `$50` into the vault:
    - The total assets increase to `$25 + $50 = $75`.
4. Then, the last slashing is executed in full:
    - The total assets decrease to `$75 - $50 = $25`.

As a result, the staker incurs a loss.

## Tools Used

Manual review

## Recommended Mitigation Steps

It is advisable to revert the deposit function if there are any pending slashing requests for the vault.


## Assessed type

Other
The `handleSlashing()` function always reverts when handling `fee-on-transfer` tokens.
Audit: 2024-07-karak-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-karak/blob/main/src/SlashingHandler.sol#L52-L59


# Vulnerability details

## Impact

Slashing is impossible, which leads to the breakdown of the protocol's core functionality.

## Proof of Concept

Consider `L56` and `L58` of the `handleSlashing()` function. At `L56`, tokens are received from the `msg.sender` (the Vault), and then transferred to `address(0)` at `L58`. The amount transferred to `address(0)` is intended to match the amount sent by the `Vault`. However, when using `fee-on-transfer` tokens, the actual amount received from the `Vault` is less than the specified `amount`, resulting in a revert at `L58` due to insufficient tokens.

https://github.com/code-423n4/2024-07-karak/blob/main/src/SlashingHandler.sol#L52-L59
```solidity
    function handleSlashing(IERC20 token, uint256 amount) external {
        if (amount == 0) revert ZeroAmount();
        if (!_config().supportedAssets[token]) revert UnsupportedAsset();

56      SafeTransferLib.safeTransferFrom(address(token), msg.sender, address(this), amount);
        // Below is where custom logic for each asset lives
58      SafeTransferLib.safeTransfer(address(token), address(0), amount);
    }
```

## Tools Used

Manual review

## Recommended Mitigation Steps

It is recommended to use `balanceAfter - balanceBefore` pattern when handling `fee-on-transfer` tokens.


## Assessed type

Other
The `Vault` contract exhibits incorrect behavior when handling `fee-on-transfer` tokens.
Audit: 2024-07-karak-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-karak/blob/main/src/Vault.sol#L78-L103
https://github.com/vectorized/solady/blob/main/src/tokens/ERC4626.sol#L376-L380
https://github.com/vectorized/solady/blob/main/src/tokens/ERC4626.sol#L192-L204


# Vulnerability details

## Impact

The calculation of the mint amount is incorrect when using `fee-on-transfer` tokens, resulting in a loss of funds for the original stakers.

## Proof of Concept

The deposit function of the Vault contract adds only the zero-amount check to that of ERC4626. However, the [deposit()](https://github.com/code-423n4/2024-07-karak/blob/main/src/Vault.sol/L78-L103) function of ERC4626 does not account for fee-on-transfer tokens. It first calculates the shares to be newly minted based on the amount sent by the staker, before receiving the underlying assets. However, when using `fee-on-transfer` tokens, the actual amount received is less than the amount sent. As a result, the newly minted share amount exceeds what corresponds to the actual received amount, leading to a loss of funds for the original stakers.

https://github.com/code-423n4/2024-07-karak/blob/main/src/Vault.sol/L78-L103
```solidity
    function deposit(uint256 assets, address to)
        public
        override(ERC4626, IVault)
        whenFunctionNotPaused(Constants.PAUSE_VAULT_DEPOSIT)
        nonReentrant
        returns (uint256 shares)
    {
        if (assets == 0) revert ZeroAmount();
@>      return super.deposit(assets, to);
    }

    function deposit(uint256 assets, address to, uint256 minSharesOut)
        external
        nonReentrant
        whenFunctionNotPaused(Constants.PAUSE_VAULT_DEPOSIT_SLIPPAGE)
        returns (uint256 shares)
    {
        if (assets == 0) revert ZeroAmount();
@>      shares = super.deposit(assets, to);
        if (shares < minSharesOut) revert NotEnoughShares();
    }
```

https://github.com/vectorized/solady/blob/main/src/tokens/ERC4626.sol#L376-L380
```solidity
    function deposit(uint256 assets, address to) public virtual returns (uint256 shares) {
        if (assets > maxDeposit(to)) _revert(0xb3c61a83); // `DepositMoreThanMax()`.
378     shares = previewDeposit(assets);
        _deposit(msg.sender, to, assets, shares);
    }
```

Let's consider the following calculations:

**Note:** The exchange rate formula is `(totalAssets + 1) / (totalSupply + 1)` as defined in [ERC4626](https://github.com/vectorized/solady/blob/main/src/tokens/ERC4626.sol#L201).

Assume that the underlying asset has a 5% transfer fee.
1. Current state of the vault:
    - `totalAssets`: 95
    - `totalSupply`: 100(all owned by Alice)
2. Bob deposits 960 assets, but only `960 * 95% = 912` actually arrive at the vault.
    - Bob's share: 960 * (100 + 1) / (95 + 1) = 1010 (the mint amount is calculated based on the sent amount of 100)
    - `totalSupply`: 100 + 1010 = 1110
    - `totalAssets`: 95 + 912 = 1007

As a result, Alice's underlying asset amount is `100 * (1007 + 1) / (1110 + 1) = 90`
A snapshot may face a permanent DoS if both a slashing event occurs in the NativeVault and the staker's validator is penalized.
Audit: 2024-07-karak-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-karak/blob/main/src/NativeVault.sol#L126-L162
https://github.com/code-423n4/2024-07-karak/blob/main/src/NativeVault.sol#L476-L495
https://github.com/code-423n4/2024-07-karak/blob/main/src/NativeVault.sol#L517-L525
https://github.com/code-423n4/2024-07-karak/blob/main/src/NativeVault.sol#L507-L515


# Vulnerability details

## Impact

The staker can still receive rewards even if their validators are empty.

## Proof of Concept

Let's consider the following scenario:

1. Alice has a validator whose balance is 32 ETH.
2. A slashing event occurs in the NativeVault, resulting in Alice being slashed by 2 ETH, reducing her withdrawable assets to 30 ETH.
3. Subsequently, Alice's validator loses all its funds (this can happen within the 7-day snapshot period, even within just 1 hour). A snapshot should then be taken to reduce Alice's assets by 32 ETH, which requires calling the `validateSnapshotProofs()` function for Alice's validator.

    ```solidity
        function validateSnapshotProofs(
            address nodeOwner,
            BeaconProofs.BalanceProof[] calldata balanceProofs,
            BeaconProofs.BalanceContainer calldata balanceContainer
        )
            external
            nonReentrant
            nodeExists(nodeOwner)
            whenFunctionNotPaused(Constants.PAUSE_NATIVEVAULT_VALIDATE_SNAPSHOT)
        {
            NativeVaultLib.Storage storage self = _state();
            NativeVaultLib.NativeNode storage node = self.ownerToNode[nodeOwner];
            NativeVaultLib.Snapshot memory snapshot = node.currentSnapshot;

            if (node.currentSnapshotTimestamp == 0) revert NoActiveSnapshot();

            BeaconProofs.validateBalanceContainer(snapshot.parentBeaconBlockRoot, balanceContainer);

            for (uint256 i = 0; i < balanceProofs.length; i++) {
                NativeVaultLib.ValidatorDetails memory validatorDetails =
                    node.validatorPubkeyHashToDetails[balanceProofs[i].pubkeyHash];

                if (validatorDetails.status != NativeVaultLib.ValidatorStatus.ACTIVE) revert InactiveValidator();
                if (validatorDetails.lastBalanceUpdateTimestamp >= node.currentSnapshotTimestamp) {
                    revert ValidatorAlreadyProved();
                }

    153         int256 balanceDeltaWei = self.validateSnapshotProof(
                    nodeOwner, validatorDetails, balanceContainer.containerRoot, balanceProofs[i]
                );

                snapshot.remainingProofs--;
    158         snapshot.balanceDeltaWei += balanceDeltaWei;
            }

    161     _updateSnapshot(node, snapshot, nodeOwner);
        }
    ```
    In this call, `snapshot.balanceDeltaWei` is set to -32 ETH, and the `_updateSnapshot()` function is invoked at `L161`.

4. In the `_updateSnapshot()` function, the `_updateBalance()` function is invoked at `L490`, with `totalDeltaWei = -32 ETH`, as noted at `L482`.
    ```solidity
        function _update