xanoutas/solidity-security-findings huggingface dataset:
Hot Signer Can Pass Arbitrary Data to Partially Whitelisted Functions
Audit: 2024-10-kleidi-findings
Severity: HIGH

# Lines of code

https://github.com/solidity-labs-io/kleidi/blob/0d72b6cb5725c1380212dc76257da96fcfacf22f/src/Timelock.sol#L1028-L1155
https://github.com/solidity-labs-io/kleidi/blob/0d72b6cb5725c1380212dc76257da96fcfacf22f/src/Timelock.sol#L1189-L1243
https://github.com/solidity-labs-io/kleidi/blob/0d72b6cb5725c1380212dc76257da96fcfacf22f/src/Timelock.sol#L708-L726
https://github.com/solidity-labs-io/kleidi/blob/0d72b6cb5725c1380212dc76257da96fcfacf22f/src/Timelock.sol#L728-L755


# Vulnerability details

## Description

The [addCalldataCheck()](https://github.com/solidity-labs-io/kleidi/blob/0d72b6cb5725c1380212dc76257da96fcfacf22f/src/Timelock.sol#L841-L855) and [removeCalldataCheck()](https://github.com/solidity-labs-io/kleidi/blob/0d72b6cb5725c1380212dc76257da96fcfacf22f/src/Timelock.sol#L857-L869) functions allow the timelock to add and remove calldatas for different index ranges of functions.

However, this mechanism contains a vulnerability where hot signers can exploit non-whitelisted parameters in function calls.

For example, if the timelock adds calldata for specific index range of a function, covering only the first parameter of that function, the hot signer can send arbitrary data for the remaining parameters even if this is not how safe owners what planned to do.

Similarly, if timelock removes index range and calldatas of a function that covers specific parameter, hot signer can send arbitrary data for that parameter, once index range covering that parameter is removed.

## Impact

when timelock adds calldata or removes specific index range of a function, Hot signers gain ability to execute those functions with passing arbitrary values for parameters that do not have any coverage.

## Proof of Concept

1. **Add Calldata Example:**  
   
   the `addCalldataCheck()` function, whitelists multiple calldatas for single index range.

lets imagine we have `someFunc(address, uint256)` and timelock wants to whitelist both parameters, with `addCalldataCheck()` function.

   to do so:

   * to whitelist `address` parameter: the `startIndex == 4` and `endIndex == 36`, and calldata is `0x4838B106FCe9647Bdf1E7877BF73cE8B0BAD5f97`. 
   * to whitelist `uint256` parameter: the `startIndex == 37` and `endIndex == 68`, and calldata is `0xde0b6b3a7640000`(1e18 in decimal). 

   now keep in mind the `addCalldataCheck()` function, whitelists multiple calldatas for single index range meaning a single parameter (it can cover all parameters in a single index range, but that's not how it's recommended to be used on [#L1102-L1108](https://github.com/solidity-labs-io/kleidi/blob/0d72b6cb5725c1380212dc76257da96fcfacf22f/src/Timelock.sol#L1102-L1108), since it reduces flexibility), that means safe is going to [schedule()](https://github.com/solidity-labs-io/kleidi/blob/0d72b6cb5725c1380212dc76257da96fcfacf22f/src/Timelock.sol#L512-L539) two operations for Timelock, to be able to whitelist both of this calldatas for that function.

   now lets imagine safe did
Lack of Pause Status Check in _updatePauseDuration Function
Audit: 2024-10-kleidi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-10-kleidi/blob/ab89bcb443249e1524496b694ddb19e298dca799/src/ConfigurablePause.sol#L104


# Vulnerability details

## Vulnerability Details

The `_updatePauseDuration` function updates the pause duration without verifying if the contract is in a paused state. While the function is meant to be called when the contract is unpaused, it lacks explicit enforcement of this condition. This omission could lead to unintended behavior, such as resetting the `pauseStartTime` while the contract is paused, effectively unpausing it unintentionally.

## Impact
The contract can be unintentionally unpaused if `_updatePauseDuration` is called while the contract is paused, allowing bypass of the enforced pause period.

## Proof of Concept

1. Assume the contract is paused, with a valid `pauseStartTime` and `pauseDuration`.
2. If this function is called while the contract is paused, the following behavior occurs:
   - The `pauseStartTime` is reset to 0.
   - The contract becomes unpaused unexpectedly.

## Recommended Mitigation Steps

Enforce a check to ensure that `_updatePauseDuration` is only called when the contract is unpaused. This can be achieved by using the `whenNotPaused` modifier or adding an explicit validation check.

**Fix using `whenNotPaused` modifier:**

```solidity
                                                                     
function _updatePauseDuration(uint128 newPauseDuration) internal whenNotPaused {
    require(
        newPauseDuration >= MIN_PAUSE_DURATION
            && newPauseDuration <= MAX_PAUSE_DURATION,
        "ConfigurablePause: pause duration out of bounds"
    );

    uint256 oldPauseDuration = pauseDuration;
    pauseDuration = newPauseDuration;

    emit PauseDurationUpdated(oldPauseDuration, pauseDuration);
}
```

**Fix with explicit pause check:**

```solidity
function _updatePauseDuration(uint128 newPauseDuration) internal {
(+) require(!paused(), "ConfigurablePause: contract is paused");
    require(
        newPauseDuration >= MIN_PAUSE_DURATION
            && newPauseDuration <= MAX_PAUSE_DURATION,
        "ConfigurablePause: pause duration out of bounds"
    );

    uint256 oldPauseDuration = pauseDuration;
    pauseDuration = newPauseDuration;

    emit PauseDurationUpdated(oldPauseDuration, pauseDuration);
}
```


## Assessed type

Invalid Validation
Invalid Validation in `_addCalldataCheck` is causing dead bytes in payload
Audit: 2024-10-kleidi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-10-kleidi/blob/ab89bcb443249e1524496b694ddb19e298dca799/src/Timelock.sol#L1119-L1123
https://github.com/code-423n4/2024-10-kleidi/blob/ab89bcb443249e1524496b694ddb19e298dca799/src/BytesHelper.sol#L50-L55


# Vulnerability details

## Vulnerability Details

In Kleidi protocol, whitelisting a calldata feature is used for restricting the parameters of the specific functions at specific external contracts. Hot signers only can use whitelisted parameters. The calldata indexes are defined as bytes and the overlapping between the checks are not allowed for security considerations. This check is applied in following line:

```solidity
                require(
                    startIndex > indexes[i].endIndex
                        || endIndex < indexes[i].startIndex,
                    "CalldataList: Partial check overlap"
                );
```

This check is prevent overlapping between the calldata checks. But this check doesn't correctly handle the edge case for the `endIndex`. Let say we have a function which takes two parameter as follows:

```test(uint256 parameter1, bytes32 parameter2)```

The calldata payload for this function will be 4 bytes selector + 32 bytes parameter1 + 32 bytes parameter2. As we mentioned before it's bytes type parameter and it slice the bytes data using indexes as follows in BytesHelper.sol contract:


```solidity
        uint256 length = end - start;
        bytes memory sliced = new bytes(length);

        for (uint256 i = 0; i < length; i++) {
            sliced[i] = toSlice[i + start];
        }
```

Let's continue with our example, our parameter1's startIndex should be 4 because selector is represented in [0,3] (both inclusive) range. Our length for uint256 parameter will be 32 and the `endIndex` for parameter1 should be 4 + 32 = 36. The range of parameter will be [4,36). The index 36 is exclusive in here because if we make both 4 and 36 inclusive the length will be 33. Basicly, index 36 is not used in here by our first calldata check. When user want to restrict the bytes32 parameter the index range for parameter2 should be [36,68). But we can't use index 36 in here because it is the `endIndex` of our first calldata check.

```solidity
                require(
                    startIndex > indexes[i].endIndex
                        || endIndex < indexes[i].startIndex,
                    "CalldataList: Partial check overlap"
                );
```

This vulnerability will cause dead bytes between the parameters. If we have n amount of parameters and we want to restrict the parameters for all of them the dead bytes amount will be n - 1.

## Impact

Those dead bytes will cause wildcarded parameters because if a selector in a contract is restricted by only 1 parameter the all the other parameters is wildcarded by default which is a design choice for the protocol. We can understand that behaviour in following calldata check:

```solidity
        require(
DoS in `pause()` function in certain conditions
Audit: 2024-10-kleidi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-10-kleidi/blob/ab89bcb443249e1524496b694ddb19e298dca799/src/Timelock.sol#L687-L700


# Vulnerability details

## Proof of Concept
The pause function in timelock is critical because it's designed to slow down the attacker. It's stated with following lines:

> Wrench Resistant: One of the guiding principles of this system is to be resistant to $5 wrench attacks. Even if an attacker is able to coerce a user into signing a transaction, the system of recovery spells and guardians slows down an attacker trying to steal funds. With a 30 day timelock as the delay on new transactions, an attacker would need to kidnap a user for a month and remain undetected for the entire period in order to steal funds.

The current implementation in pause, removes the live proposals and then pause all the operations in the timelock contract. The problem in here, we don't know how many live proposals are in the timelock.

Actually, if our cold signers removes the expired proposals using clean feature, it won't be a problem. But there is no guarantee that the users will use clean feature regularly. 

In the following PoC, we can check the gas usage of pausing and cleaning 100 proposals from the list.

```solidity
    function testScheduleProposalSafeSucceedsWithParam(uint256 salt) public{
        _schedule({
            caller: address(safe),
            timelock: address(timelock),
            target: address(timelock),
            value: 0,
            data: abi.encodeWithSelector(
                timelock.updateDelay.selector, MINIMUM_DELAY
            ),
            salt: bytes32(salt),
            delay: MINIMUM_DELAY
        });

    }
    function test_DoSInPause() public {
        for(uint i = 0; i < 100; i++){
            testScheduleProposalSafeSucceedsWithParam(i); 
        }

        vm.startPrank(guardian);
        uint256 gasLeft = gasleft();
        timelock.pause();
        console.log("Consumed gas for 100 proposal approx: %s", gasLeft - gasleft());
    }
```

The output will be 334373 gas for 100 proposals. The current block gas limit in Ethereum is 30 Million. In conclusion if the users don't clean their expired proposals regularly and if it reachs 10000 live proposals, it can revert on pause due to block gas limit.

In the impact part, the pause feature is critical because it's time sensitive and it should be executed immediately in emergency situations. In conclusion, DoS in pause function can have massive impacts on the contract such as loss of funds.

## Recommended Mitigation Steps

In every schedule call by gnosis safe removing all the expired executions from the live proposals can save the contract from DoS.









## Assessed type

DoS
The pause function in the Timelock contract could be temporarily subjected to a DoS attack due to the unbounded iteration over the liveProposals array.
Audit: 2024-10-kleidi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-10-kleidi/blob/ab89bcb443249e1524496b694ddb19e298dca799/src/Timelock.sol#L687-L700


# Vulnerability details

## Proof of Concept

The Timelock contract has a `pause` function that pauses and deletes all `activeProposals` that were scheduled before or have already expired but were not cleaned up (https://github.com/code-423n4/2024-10-kleidi/blob/ab89bcb443249e1524496b694ddb19e298dca799/src/Timelock.sol#L691-L692). Because we simply iterate over all `liveProposals` without any restrictions (an unbounded array), this means that more proposals result in a more expensive `pause` function, and in some circumstances, this could become an issue. For example:

1. One of the HotSigner's private keys is compromised, and a malicious user can now schedule their transactions.
2. The attacker schedules many transactions, making the `liveProposals` array very large.
3. The `PauseGuardian` tries to call `pause` to stop the spam, allowing time to address the leaked private key.
4. Since `liveProposals` already contains over 1,000 entities, the loop that deletes each entity exceeds the available block gas limit for the current chain, causing the `pause` transaction to fail.

The only available option is to cancel or clean up those proposals manually. However, the problem is that the Timelock allows for the deletion of only one proposal (by id), and there is no batch cancel function. This means deleting entities one by one is ineffective because the attacker can spam transactions at the same or a higher frequency.

## Recommended Mitigation Steps

Restrict the number of proposals a user can schedule, and add a function to cancel or clean up several proposal IDs at once.


## Assessed type

DoS
The calldata checks which don't overlap partially can't be added.
Audit: 2024-10-kleidi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-10-kleidi/blob/main/src/Timelock.sol#L1120-L1121


# Vulnerability details

## Proof of Concept
[Root cause]
`Timelock._addCalldataCheck()` function is the following.
```solidity
    function _addCalldataCheck(
        address contractAddress,
        bytes4 selector,
        uint16 startIndex,
        uint16 endIndex,
        bytes[] memory data
    ) private {
        --- SKIP ---
            bool found;
            for (uint256 i = 0; i < indexes.length; i++) {
                if (
                    indexes[i].startIndex == startIndex
                        && indexes[i].endIndex == endIndex
                ) {
                    targetIndex = i;
                    found = true;
                    break;
                }
                /// all calldata checks must be isolated to predefined calldata segments
                /// for example given calldata with three parameters:

                ///                    1.                              2.                             3.
                ///       000000000000000112818929111111
                ///                                     000000000000000112818929111111
                ///                                                                   000000000000000112818929111111

                /// checks must be applied in a way such that they do not overlap with each other.
                /// having checks that check 1 and 2 together as a single parameter would be valid,
                /// but having checks that check 1 and 2 together, and then check one separately
                /// would be invalid.
                /// checking 1, 2, and 3 separately is valid
                /// checking 1, 2, and 3 as a single check is valid
                /// checking 1, 2, and 3 separately, and then the last half of 2 and the first half
                /// of 3 is invalid

                require(
1121:               startIndex > indexes[i].endIndex
1122:                   || endIndex < indexes[i].startIndex,
                    "CalldataList: Partial check overlap"
                );
            }
        --- SKIP ---
    }
```
As can be seen, `L1121` uses strict inequality `>` instead of `>=` and `L1122` uses strict inequality '<' instead of `<=`. Therefore, the function will revert if the two calldatas are for two consecutive parameters respectively.

[Proof of Concept]
For instance, assume that the `contractAddress` is the address of the following [MockLending](https://github.com/code-423n4/2024-10-kleidi/blob/main/test/mock/MockLending.sol) contract and the `selector` is for the `deposit()` function.
```solidity
contract MockLending {
    mapping(address owner => uint256 amount) _balance;

    function deposit(address to, uint256 amount) external {
        _balance[to] += amount;

        /// token.transferFrom(msg.sender, address(this), amount);
    }
    --- SKIP ---
}
```
Then, in the calldata to `deposit()` function, `0~3`
Some bytes may be emitted when we check the call data
Audit: 2024-10-kleidi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-10-kleidi/blob/ab89bcb443249e1524496b694ddb19e298dca799/src/BytesHelper.sol#L35-L58


# Vulnerability details

## Proof of Concept
In Timelock contract, we can assign hot assign's valid behaviour via `addCalldataChecks`. The hot signers can execute the whitelist calldata transaction.
The problem is that some bytes in the whole calldata will be missed in the calldata check. This will decrease the calldata check's safety.

When we try to add several index, we have one check to make sure there will be any overlap between two different index.
For example:
The first index is from 4 to 8. (startIndex=4, endIndex=8)
If we want to add one second index, the minimum startIndex should be 9. Imagine the second index is from 9 to 12.(startIndex=9, endIndex=12)
The problem is that when we want to check the first index, we will get one sliced bytes via `sliceBytes()`. The byte index 4,5,6,7 will be included into the return value `sliced`. The endIndex 8 for the first index is not included into the `sliced`.
When we go on checking the second index, we will get another sliced bytes, the byte index 9,10,11 will be included the second `sliced`.
From the above example, we will miss the byte index 8 in the calldata checking even if we don't leave any gap between the first index and the second index.

The impact for this is serious. Because that some input parameters cannot be checked via `checkCalldata`.
```solidity
    function _addCalldataCheck(
        address contractAddress,
        bytes4 selector,
        uint16 startIndex,
        uint16 endIndex,
        bytes[] memory data // Here the data is bytes[], 
    ) private {
        ...
                require(
                    startIndex > indexes[i].endIndex
                        || endIndex < indexes[i].startIndex,
                    "CalldataList: Partial check overlap"
                );
        ...
}
```
```solidity
    function sliceBytes(bytes memory toSlice, uint256 start, uint256 end)
        public
        pure
        returns (bytes memory)
    {
        require(
            start < toSlice.length,
            "Start index is greater than the length of the byte string"
        );
        require(
            end <= toSlice.length,
            "End index is greater than the length of the byte string"
        );
        require(start < end, "Start index not less than end index");

        uint256 length = end - start;
        bytes memory sliced = new bytes(length);
        // For example, start = 4, end = 8
        // The first byte is toSlice[start] = toSlice[4], the last byte is toSlice[7]
        for (uint256 i = 0; i < length; i++) {
            sliced[i] = toSlice[i + start];
        }

        return sliced;
    }
```

## Recommended Mitigation Steps
If we keep current `sliceBytes`'s implementation, it means that when we check one index(startIndex, endIndex), the startIndex will be included into this index calldata check and endIndex will b
It's impossible to add a calldata check for neighboring byte portions (and more implications)
Audit: 2024-10-kleidi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-10-kleidi/blob/ab89bcb443249e1524496b694ddb19e298dca799/src/Timelock.sol#L1119-L1122,https://github.com/code-423n4/2024-10-kleidi/blob/ab89bcb443249e1524496b694ddb19e298dca799/src/Timelock.sol#L496-L501


# Vulnerability details

## Summary

_addCalldataCheck and checkCalldata are not fine tuned to work correctly when checking neighboring byte portions - impossible to add two calldata checks which are neighboring.

## Vulnerability details

When adding a calllData check it does not allow to add check whose startIndex or endIndex are overlapping or EQUAL.

```
require(
    startIndex > indexes[i].endIndex // <-- any new addition CAN'T have startIndex equal endIndex
      || endIndex < indexes[i].startIndex, 
    "CalldataList: Partial check overlap"
);
```

This means the closest checks possible will look like this (example using 4 byte portions):
`{ startIndex: 4, endIndex: 8, ... }` and `{ startIndex: 9, endIndex: 13 ... }`

And on the surface this may look fine, but when we reach checkCalldata, it is calling getSliceBytesHash -> which uses sliceBytes function underneath:
```
require(
    calldataCheck.dataHashes.contains(
        data.getSlicedBytesHash(  // <-- is used to slice and hash
            calldataCheck.startIndex, calldataCheck.endIndex
        )
    ),
    "CalldataList: Calldata does not match expected value"
);
```

And if we look at sliceBytes function:

```
function sliceBytes(bytes memory toSlice, uint256 start, uint256 end) {
...
        uint256 length = end - start;
        bytes memory sliced = new bytes(length);

        for (uint256 i = 0; i < length; i++) {
            sliced[i] = toSlice[i + start]; // <-- slice a portion from start NOT including endIndex [start; end)
        }
...
}
```

It counts bytes from start index (i starts at 0) and keeps slicing (end - start -> 8 - 4 = 4) times. This means 4th,5th,6th and 7th bytes are sliced. This also means that the endIndex itself (8) is NOT going to be included in the slice.

From here we see that there is a discrepancy:
- when adding new checks -> you CAN'T set the startIndex (9) to equal any existing endIndex (8)
- when checking -> endIndex (8) is always SKIPPED in slicing

Which creates a 1 byte gap any time more than 1 check (with a different byte portion) is added to a single calldata.

## Impact

Can't add neighboring calldata checks, because the checked indexes are applied on wrong pieces of calldata bytes. Also because the slicing is applied incorrectly, the calldata check will fail if calldata is the exact size as the checks (due to 1 byte shift, needs to have an extra byte at the end).

## Tools used

Manual review + foundry tests

## Proof of Concept

Lets use an example calldata with portions as follows:
0x42424242111111112222222233

arg1 and arg2 are 4 bytes each
arg3 is a single byte

[selector] [arg1  ] [arg2  ] [arg 3]
0x42424242 11111111 22222222 33


foundry test
```
   function testAddArbitraryCheckSuccee