xanoutas/solidity-security-findings huggingface dataset:
Compromised cold signers can take over the safe by scheduling too many proposals which causes `pause()` to revert
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

# Vulnerability Details

## Impact

When the cold signers are compromised, the pause guardian calls `pause()` to cancel all malicious proposals.

By doing that, he loops over every `_liveProposal` to delete it.
This means that `pause()` will run out of gas and therefore revert, if the malicious cold signers schedule too many proposals before the pause guardian pauses.

Therefore, the contract can not be paused and the malicious proposals can be executed before the recovery spell can rotate the signing keys.

It is THE main property of a kleidi wallet that compromised cold signers can not take over the safe.

## Proof of Concept

The following test should be added to `Timelock.t.sol`:

```solidity
function testPauseDoS() public {

    for (uint256 i = 0; i < 10000; i++) {
        vm.prank(address(safe));
        timelock.schedule(
        address(timelock),
        0,
        abi.encodeWithSelector(timelock.updateDelay.selector, 30 days),
        bytes32(i),
        MINIMUM_DELAY
    );
    }

    vm.prank(guardian);
    timelock.pause();
}
```
This test schedules `10000` proposals and calls `pause()` afterwards.

Furthermore, a few lines need to be added in `Timelock.sol`:

```solidity
function pause() public override {
    /// check that msg.sender is the pause guardian, pause the contract
    super.pause();

+    console.log(gasleft());

    bytes32[] memory proposals = _liveProposals.values();
    for (uint256 i = 0; i < proposals.length; i++) {
        bytes32 id = proposals[i];

        delete timestamps[id];
        assert(_liveProposals.remove(id));

        emit Cancelled(id);
    }
+    console.log(gasleft());
}
```

Additionally, this import needs to be added on top of `Timelock.sol`:

```solidity
import {console} from "forge-std/console.sol";
```

The block gas limit on ethereum equals `30 Million` which means that any transaction that costs more gas than this will revert.
When running `forge test --mt testPauseDoS -vv` the following logs will be printed:

```
Ran 1 test for test/unit/Timelock.t.sol:TimelockUnitTest
[PASS] testPauseDoS() (gas: 1560418624)
Logs:
  18158513696823674292
  18158513696791425309

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.17s (1.16s CPU time)

Ran 1 test suite in 1.19s (1.17s CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
```
The first log equals the result of the `gasleft()` function call before entering the loop.
The second log equals the result of the `gasleft()` function call after the loop, which means all proposals have been deleted.

This means that the difference of both numbers is the amount of gas that has been used by deleting every proposal. Said difference is:
`18158513696823674292 - 18158513696791425309 = 32248983`, which is higher than `30 Million`.

Taking this into account, the following scenario
Calldata checks require an unvalidated byte in between calldata segments which leads to flawed validation
Audit: 2024-10-kleidi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

# Vulnerability Details

## Impact

When a call is whitelisted, either during the creation of the system instance or when a proposal to `addCalldataCheck()` or `addCalldataChecks()` is executed, the required data needs to be provided.

This includes, among other things, the `startIndex` and `endIndex` of each calldata segment.

During the whitelisting process, `_addCalldataCheck()` in `Timelock.sol` is called. This function checks that the `startIndex` and `endIndex` of the calldata do not partially overlap with other indexes.

However, this check incorrectly interprets the calldatas, which could result in the execution to be completely different. For example, an amount that needs to be transferred could be larger than the user intended.

## Proof of Concept

The following test needs to be added to `Timelock.t.sol`:

```solidity
function testAddCalldataOffByOnePOC() public {
    address[] memory targetAddresses = new address[](2);
    targetAddresses[0] = address(lending);
    targetAddresses[1] = address(lending);

    bytes4[] memory selectors = new bytes4[](2);
    selectors[0] = MockLending.deposit.selector;
    selectors[1] = MockLending.deposit.selector;

    /// compare first 20 bytes
    uint16[] memory startIndexes = new uint16[](2);
    startIndexes[0] = 4;
    startIndexes[1] = 5;

    uint16[] memory endIndexes = new uint16[](2);
    endIndexes[0] = 5;
    endIndexes[1] = 6;

    bytes[][] memory checkedCalldatas = new bytes[][](2);
    /* @audit
    We want to add a check for the fith byte which is "1" (start index 4),
    and a check for the sixth byte which is "2" (start index 5).
    But it's not possible due to the wrong "partial overlap" check.
    To add a check for the fith byte, the end index needs to be 5 but
    this is the same as the start index for the sixth byte.
    */
    checkedCalldatas[0] = new bytes[](1);
    checkedCalldatas[0][0] = bytes("1");
    checkedCalldatas[1] = new bytes[](1);
    checkedCalldatas[1][0] = bytes("2");

    vm.prank(address(timelock));
    vm.expectRevert("CalldataList: Partial check overlap");
    timelock.addCalldataChecks(
        targetAddresses,
        selectors,
        startIndexes,
        endIndexes,
        checkedCalldatas
    );      
}
```

The test first sets the necessary variables and then calls `addCalldataChecks()` as the timelock.

The important variables are the start and endIndexes, which are set as follows:

```
startIndexes[0] = 4;
startIndexes[1] = 5;

endIndexes[0] = 5;
endIndexes[1] = 6;
```

Currently, due to a bug in the code, it is not possible to add these two calldata segments because one unvalidated byte is required between them.

Further details are described in the comments within the test.

The cold signers would provide calldata checks in such a way that there is no revert while also expectin
Unable to create a calldata check immediately adjacent to an existing one
Audit: 2024-10-kleidi-findings
Severity: MEDIUM

# Lines of code

https://github.com/solidity-labs-io/kleidi/blob/0d72b6cb5725c1380212dc76257da96fcfacf22f/src/Timelock.sol#L1120-L1121


# Vulnerability details

## Impact

This vulnerability will prevent the creation of a calldata check map as intended.

## Proof of Concept

The `Timelock::checkCalldata()` function verifies the whitelisted status of calldata by invoking `BytesHelper.getSlicedBytesHash()` which internally calls `BytesHelper.sliceBytes()`.

[src/Timelock.sol#L495-L502](https://github.com/solidity-labs-io/kleidi/blob/0d72b6cb5725c1380212dc76257da96fcfacf22f/src/Timelock.sol#L495-L502)
```solidity
            require(
                calldataCheck.dataHashes.contains(
                    data.getSlicedBytesHash(
                        calldataCheck.startIndex, calldataCheck.endIndex
                    )
                ),
                "CalldataList: Calldata does not match expected value"
            );
```

The [`sliceBytes()`](https://github.com/solidity-labs-io/kleidi/blob/0d72b6cb5725c1380212dc76257da96fcfacf22f/src/BytesHelper.sol#L35-L58) function grabs a slice of bytes by `start` and `end` indexes.

```solidity
    function sliceBytes(bytes memory toSlice, uint256 start, uint256 end)
        public
        pure
        returns (bytes memory)
    {
        ... ...

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

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

        return sliced;
    }
```

From the above code snippet, we can see that the function takes a slice by the range `[start, end - 1]`.

However, when creating a calldata check map by using the [`Timelock::_addCalldataCheck()`](https://github.com/solidity-labs-io/kleidi/blob/0d72b6cb5725c1380212dc76257da96fcfacf22f/src/Timelock.sol#L1034-L1155)function, the function checks the overlapping status between new range and existing ones by the following lines:

```solidity
    function _addCalldataCheck(
        address contractAddress,
        bytes4 selector,
        uint16 startIndex,
        uint16 endIndex,
        bytes[] memory data
    ) private {
        ... ...
        uint256 targetIndex = indexes.length;
        {
            bool found;
            for (uint256 i = 0; i < indexes.length; i++) {
                ... ...
                require(
                    startIndex > indexes[i].endIndex
                        || endIndex < indexes[i].startIndex,
                    "CalldataList: Partial check overlap"
                );
            }
            ... ...
        }
        ... ...
    }
```

Let's assume `S1, E1` is an existing range. Then, the new range to be added can be put into either within `[E1+1, +Inf)` or `[4, S1-1]`.

But, as `sliceBytes()` takes a slice from `S1` to `E1 - 1`, the index `E1` cannot be occupied by calldata checks.

If there are several ranges, their end indexes will not be included as calldata check region.

## Tools Used

Manual Re
Kleidi's pausing functionality is broken
Audit: 2024-10-kleidi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-10-kleidi/blob/ab89bcb443249e1524496b694ddb19e298dca799/src/ConfigurablePause.sol#L59-L62
https://github.com/code-423n4/2024-10-kleidi/blob/ab89bcb443249e1524496b694ddb19e298dca799/src/ConfigurablePause.sol#L69-L71
https://github.com/code-423n4/2024-10-kleidi/blob/ab89bcb443249e1524496b694ddb19e298dca799/src/ConfigurablePause.sol#L67-L68


# Vulnerability details


## Proof of Concept

First, would be key to note that the protocol integrates with a pausing logic for contracts, this is done by the `whenNotPaused` modifier that only queries the `paused` functionality to ensure the protocol is not paused, i.e

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

```solidity
    modifier whenNotPaused() {
        require(!paused(), "Pausable: paused");
        _;
    }
```

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

```solidity
    function paused() public view returns (bool) {
        return block.timestamp &lt;= pauseStartTime + pauseDuration;
    }
```

Now per the documentation these are the cases as to which we should consider the protocol paused, quote on quote:

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

&gt; /// if pauseStartTime is 0, contract is not paused

&gt; /// if pauseStartTime is not 0, contract could be paused in the pauseDuration window

Issue however is that there are no checks on whether the `pauseStartTime` is indeed `0`, which would mean that when we have the `pauseStartTime` as `0` the modifier erroneously executes.

Even worse is when there is an update to paused state and the duration to which is currently is the functionality filly does not function.

Simplistic POC:

- `pauseDuration` is initially set as 1 day the minimum.
- Contracts get paused _this could be due to a black swan event or whatever_.
- Pause duration gets extended to 3 days _which we should expect to be a used functionality as it's present in scope, also in the case this is a black swan event and everything hasn't settled_.
- Expected functionality would be that after this increment of paused duration, the `whenNotPaused` modifier should always revert within the 3 days, however it doesn't cause when updating the paused duration, [the pause start time is erroneously set to `0`](https://github.com/code-423n4/2024-10-kleidi/blob/ab89bcb443249e1524496b694ddb19e298dca799/src/ConfigurablePause.sol#L113).
- Which then means that in the `paused` check, this `return block.timestamp &lt;= pauseStartTime + pauseDuration;` returns false, instead of true cause we would have `block.timestamp &lt;= 0 + 3days`.

In code snippets:

- When pausing, the start time is correctly set:
  https://github.com/code-423n4/2024-10-kleidi/blob/ab89bcb443249e1524496b694ddb19e298dca799/src/ConfigurableP
Calldata check can not be added for adjacent parameters
Audit: 2024-10-kleidi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-10-kleidi/blob/c474b9480850d08514c100b415efcbc962608c62/src/Timelock.sol#L1028-L1155
https://github.com/code-423n4/2024-10-kleidi/blob/c474b9480850d08514c100b415efcbc962608c62/src/Timelock.sol#L1119-L1123


# Vulnerability details

## Explanation
The [_addCalldataCheck](https://github.com/code-423n4/2024-10-kleidi/blob/c474b9480850d08514c100b415efcbc962608c62/src/Timelock.sol#L1028-L1155) function is used to enable certain addresses and selectors to be called by hot signers and run validation on their input data.
However, while checking the [overlap of parameters](https://github.com/code-423n4/2024-10-kleidi/blob/c474b9480850d08514c100b415efcbc962608c62/src/Timelock.sol#L1119-L1123), the validation is too restrictive, and results in making it impossible for two adjacent parameters to be added independently.
```solidity
                require(
                    startIndex > indexes[i].endIndex
                        || endIndex < indexes[i].startIndex,
                    "CalldataList: Partial check overlap"
                );
```
This happens because the `startIndex` is supposed to be inclusive, and the `endIndex` is exclusive. As an example, if the two first inputs of a function are of types `address`, the start and end index of the first one should be [4, 24), and the next variables should be at indexes [24, 44) of the calldata. And since the start index of the second variable is equal to the end index of the first variable, the check fails.

## Impact
The comments in the code clearly mention that the [separate checks should be possible](https://github.com/code-423n4/2024-10-kleidi/blob/c474b9480850d08514c100b415efcbc962608c62/src/Timelock.sol#L1114). While this is still possible to do, the users need to merge all of the adjacent addresses they need together and add all of the accepted string separately. This will make the management of such calldata much harder than it should be.

As an example, for a function call `signature(dataType, dataType, dataType, dataType, dataType)`, if for each of the first 4 fields there are 5 accepted strings, user should enter `4^5 = 1024` strings. And if at a later date they want to add a restriction on the 5th data type, they need to remove all the previous ones and add all the strings from the start. This problem increases exponentially with the number of inputs and their possible values, and can make it impossible for some situations to add the values. 10 parameters with 10 different values would take `10^10` restrictions in total which can be quite impossible for users to add.


## Proof of Concept
The `testWhitelistingBatchCalldataSucceeds` test is slightly modified as below and can show that adjacent variables can not be added. The result fails with `revert: CalldataList: Partial check overlap`:
```solidity
    function testWhitelistingBatchCalldataSucceeds() public returns (address) {
        address[] memory targets = new address[](1);
        targ
Pause function can be bricked
Audit: 2024-10-kleidi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Explanation
The [`pause`](https://github.com/code-423n4/2024-10-kleidi/blob/c474b9480850d08514c100b415efcbc962608c62/src/Timelock.sol#L685-L700) function in the timelock is looping through all the proposals and cancels them all in case it is called by guardian. However, the function can be bricked if there are too many proposals.
This is important as one of the main use cases of Kleidi vs using a normal GnosisSafe wallet, is that the pause guardian can step in and singlehandedly stop the system while the recovery spell is passing it's time.

## Impact
[As mentioned in the project documentation](https://github.com/code-423n4/2024-10-kleidi/blob/c474b9480850d08514c100b415efcbc962608c62/docs/EDGECASES.md#guardian), in case the cold signers are compromised, the pause guardian can step in and pause the functionality of the timelock before recovering the safe with a `Recoveryspell`.

Situation 1, cold signers gone rogue: In the case that more than `owners.length - threshold` have gone rogue, and the victim signers can not cancel anything on the timelock, the rogue signers can create many proposals on the timelock, effectively cutting the guardian out of the loop as well. This is because if the gas usage of the `pause()` function is more than the block's max gas limit, it would effectively become uncallable and will always fail with `out of gas` error.

Situation 2, cold signers' key are stolen: In this case cold signers should be able to cancel all proposals one by one.

## Cost of Attack
The cost of this attack depends of the gas usage of `schedule`, `cancel` functions beside the block max gas limit. 
As shown in the proof of concept below, the gas limit of `cancel` is somewhat equal to 5% of the gas cost of `schedule`. Therefore, attackers should fill up to 20 blocks before the guardian is cut out of the equation.
Here, we explore the cost of this attack on Optimism, with `60,000,000` block gas limit. Filling up 20 blocks is equal to `20*60,000,000 = 1,200,000,000` gas usage. The attack can be executed in less that a minute on optimism, but still the attack can be caught if the pause guardian is extremely fast, therefore, the cost of attack is:
```
C = 1,200,000,000 gas on OP network
P = Profit from taking over the safe
R = Risk of getting caught mid attack
Overall profit = P - (C + R)
```

In case the overall profit is more than zero, the attack can be profitable.


## Proof of Concept
The PoC below, shows the gas consumption increase based on the number of proposal increased. In case the gas usage of the `pause()` function goes above the block limit on any network, the transaction would fail:
```solidity
    function testGasConsumption() public {

        bytes32 scheduleSalt = bytes32("saltxyz");
        uint256 numOfProposals = 100000;
        bytes32[] memory saltArray = new bytes32[
Gas griefing/attack via creating the proposals
Audit: 2024-10-kleidi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-10-kleidi/blob/c474b9480850d08514c100b415efcbc962608c62/src/Timelock.sol#L512-L539
https://github.com/code-423n4/2024-10-kleidi/blob/c474b9480850d08514c100b415efcbc962608c62/src/Timelock.sol#L652-L665


# Vulnerability details

## Explanation
The timelock acts in a way that once the proposals are submitted, they need to be cancelled or executed. This behaviour opens up a griefing attack vector towards the owners of the vault in case at least `threshold` amount of owners' private keys are exposed. 

When the keys are exposed, the attackers can send as many transactions as they need to the network from the safe with different salts. Even if one of the transactions go through, funds can be stolen. The protocol defence mechanisms in these situations is (1) Pause guardian can cancel all the proposals (2) Cold signers can cancel proposals.
Both these defence mechanisms require gas usage from the victim's accounts, and **it is important to note that they can not use the funds inside the Kleidi wallet**. This can lead to a gas war between attackers and the victims and can cause them to at least cause a griefing attack.

## Impact
Assumption in this section is that the victims do not get external help and they have invested most of their liquidity inside Kleidi, and only kept minimal amounts out for gas payments.

- Imagine if victims have access to `F` amounts of funds, and 95% of those funds is locked into Kleidi.
- The proof of concept below shows that the gas consumption of `cancel` is close to 5% of `schedule`.
- In case the keys are compromised, attackers can send many transactions spending `G` amount of gas. The requires the victims to need to spend `0.05 * G` in gas to cancel those proposals.
- The reward for attackers, only if one of their transactions go through, is `0.95 * F`.
- Given that victims only have access to `0.05 * F` to pay for `0.05 * G`, if attackers pay more than the funds inside the protocol, meaning (`G > F`), they can claim the funds in the protocol and drain it as victims do not have enough funds to cancel all proposals.

At the end, attackers can re-claim most of what they spent. Overall spending `G - 0.95 * F = G - 0.95 * G = 0.05 * G`, and steal `0.95 * G` from the user.

Note: In case the victims have invested more than `~95%` into the Kleidi, attackers will be able to make profit.

## Proof of Concept
Gas consumptions is thoroughly investigated in the test below:
```solidity
    function testGasConsumption() public {

        bytes32 scheduleSalt = bytes32("saltxyz");
        uint256 numOfProposals = 100000;
        bytes32[] memory saltArray = new bytes32[](numOfProposals);

        for(uint i; i < numOfProposals; i++) {
            saltArray[i] = keccak256(abi.encodePacked("salt", bytes32(i + 1)));
        }

        bytes memory scheduleData = abi.encode(timelock.updateDelay, MINIMUM_DELAY);
        address timelockAddress = address(timelock);


        // initial cal
If the recovery owner is compromised, the wallet can be stolen and drained by the attacker in certain circumstances
Audit: 2024-10-kleidi-findings
Severity: MEDIUM

# Lines of code

https://github.com/solidity-labs-io/kleidi/blob/0d72b6cb5725c1380212dc76257da96fcfacf22f/src/RecoverySpellFactory.sol#L118-L138


# Vulnerability details

## Proof of Concept

Currently, recovery spells are used for safety of the wallet users. If the current owner of the Gnosis safe is compromised, recovery spells can be used for swapping the owners with the new owners.

But the current system design still takes some risks because it allows to deploy a recovery spell with 1 owner. If the that recovery owner is compromised then attacker can take the control of the Gnosis safe immediately in this situation

```solidity
    function _paramChecks(
        address[] memory owners,
        uint256 threshold,
        uint256 recoveryThreshold,
        uint256 delay
    ) private pure {
        require(
            threshold <= owners.length,
            "RecoverySpell: Threshold must be lte number of owners"
        );
        require(
            recoveryThreshold <= owners.length,
            "RecoverySpell: Recovery threshold must be lte number of owners"
        );

        require(threshold != 0, "RecoverySpell: Threshold must be gt 0");
        require(delay <= 365 days, "RecoverySpell: Delay must be lte a year");
        for (uint256 i = 0; i < owners.length; i++) {
            require(owners[i] != address(0), "RecoverySpell: Owner cannot be 0");
        }
    }
```

Using only 1 owner and allowing the value 1 as threshold is not safe for the wallet owners. After compromising, the attacker will be the new owner of the Gnosis safe and he can stole the tokens and ETH from the wallet.

## Recommended Mitigation Steps

The threshold for recovery spell should be 2 at least in order to reduce the risk. Maybe 1 owner can be compromised but the possibility of compromising of the both 2 owners is significantly low.



## Assessed type

Access Control