xanoutas/solidity-security-findings huggingface dataset:
`RecoverySpellFactory::_paramChecks` doesn't check for `SENTINEL_OWNERS` making it possible to create unusable recovery spells
Audit: 2024-10-kleidi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-10-kleidi/blob/main/src/RecoverySpellFactory.sol#L118-L138


# Vulnerability details

## Decsription

Recovery spells are custom recovery mechanisms that can be used to recover funds in case of lost keys. They can be used to rotate the signers of a multisig, or to create other custom recovery logic. They are created after the wallet/safe creation, but their addresses are predicted and added beforehand to the wallet/safe creation, this logic is handled in `RecoverySpellFactory`.

When a recovery spell is executed, it removes/swaps/adds the safe owners, this logic is done in the [Owner module](https://github.com/safe-global/safe-smart-account/blob/v1.3.0/contracts/base/OwnerManager.sol), and some checks are done upon owners' rotation, one of them is that the new owner shouldn't be the `SENTINEL_OWNERS` which refers to the head of the linked list, else the rotation will revert.
```solidity
require(owner != address(0) && owner != SENTINEL_OWNERS && owner != address(this), "GS203");
```

However, this check is not available in the `RecoverySpellFactory`, this allows a malicious owner to construct, inject, and deploy a recovery spell that can never be used.


## Proof of Concept

Add the following test in `test/integration/RecoverySpells.t.sol`, and run it using `forge test -vv --fork-url "https://mainnet.infura.io/v3/PROJECT_ID" --fork-block-number 20515328 --mt test_NotValidatingSENTINELOwner`:

```solidity
address SENTINEL = address(0x1);
address PREDICTED_WALLET_ADDRESS =
    0xF5BFbBb117CecEB3911084D47d047b380a7E4Eba;

function test_NotValidatingSENTINELOwner() public {
    NewInstance memory instance;

    address owner = vm.addr(11);
    address owner2 = vm.addr(12);

    // Wallet owners
    address[] memory owners = new address[](1);
    owners[0] = owner;

    // Recovery spell owners, contains SENTINEL
    address[] memory recoveryOwners = new address[](2);
    recoveryOwners[0] = owner2;
    recoveryOwners[1] = SENTINEL;

    address[] memory hotSigners = new address[](1);
    hotSigners[0] = HOT_SIGNER_ONE;

    // Prepare recovery spells
    address[] memory recoverySpells = new address[](1);
    recoverySpells[0] = address(
        recoveryFactory.calculateAddress(
            bytes32(0),
            recoveryOwners,
            PREDICTED_WALLET_ADDRESS,
            1,
            1,
            1
        )
    );

    // Deploy wallet
    vm.prank(HOT_SIGNER_ONE);
    SystemInstance memory wallet = deployer.createSystemInstance(
        NewInstance({
            owners: owners,
            threshold: 1,
            recoverySpells: recoverySpells,
            timelockParams: DeploymentParams(
                MIN_DELAY,
                EXPIRATION_PERIOD,
                guardian,
                PAUSE_DURATION,
                hotSigners,
                new address[](0),
                new bytes4[](0),
                new uint16[](0),
                new uint16[](0),
                new
`RecoverySpellFactory::_paramChecks` doesn't check if an owner is the same as the safe making it possible to create unusable recovery spells
Audit: 2024-10-kleidi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-10-kleidi/blob/main/src/RecoverySpellFactory.sol#L118-L138


# Vulnerability details

## Description

Recovery spells are custom recovery mechanisms that can be used to recover funds in case of lost keys. They can be used to rotate the signers of a multisig, or to create other custom recovery logic. They are created after the wallet/safe creation, but their addresses are predicted and added beforehand to the wallet/safe creation, this logic is handled in `RecoverySpellFactory`.

When a recovery spell is executed, it removes/swaps/adds the safe owners, this logic is done in the [Owner module](https://github.com/safe-global/safe-smart-account/blob/v1.3.0/contracts/base/OwnerManager.sol), and some checks are done upon owners' rotation, one of them is that the new owner shouldn't be the safe itself, else the rotation will revert.
```solidity
require(owner != address(0) && owner != SENTINEL_OWNERS && owner != address(this), "GS203");
```

However, this check is not available in the `RecoverySpellFactory`, this allows a malicious owner to construct, inject, and deploy a recovery spell that can never be used.

## Proof of Concept

Add the following test in `test/integration/RecoverySpells.t.sol`, and run it using `forge test -vv --fork-url "https://mainnet.infura.io/v3/PROJECT_ID" --fork-block-number 20515328 --mt test_NotValidatingSafeOwner`:

```solidity
address SENTINEL = address(0x1);
address PREDICTED_WALLET_ADDRESS =
    0xF5BFbBb117CecEB3911084D47d047b380a7E4Eba;

function test_NotValidatingSafeOwner() public {
    NewInstance memory instance;

    address owner = vm.addr(11);
    address owner2 = vm.addr(12);

    // Wallet owners
    address[] memory owners = new address[](1);
    owners[0] = owner;

    // Recovery spell owners, contains PREDICTED_WALLET_ADDRESS
    address[] memory recoveryOwners = new address[](2);
    recoveryOwners[0] = owner2;
    recoveryOwners[1] = PREDICTED_WALLET_ADDRESS;

    address[] memory hotSigners = new address[](1);
    hotSigners[0] = HOT_SIGNER_ONE;

    // Prepare recovery spells
    address[] memory recoverySpells = new address[](1);
    recoverySpells[0] = address(
        recoveryFactory.calculateAddress(
            bytes32(0),
            recoveryOwners,
            PREDICTED_WALLET_ADDRESS,
            1,
            1,
            1
        )
    );

    // Deploy wallet
    vm.prank(HOT_SIGNER_ONE);
    SystemInstance memory wallet = deployer.createSystemInstance(
        NewInstance({
            owners: owners,
            threshold: 1,
            recoverySpells: recoverySpells,
            timelockParams: DeploymentParams(
                MIN_DELAY,
                EXPIRATION_PERIOD,
                guardian,
                PAUSE_DURATION,
                hotSigners,
                new address[](0),
                new bytes4[](0),
                new uint16[](0),
                new uint16[](0),
                new bytes[][](0),
Wrong handling of call data check indices, forcing it sometimes to revert
Audit: 2024-10-kleidi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-10-kleidi/blob/main/src/Timelock.sol#L1136
https://github.com/code-423n4/2024-10-kleidi/blob/main/src/BytesHelper.sol#L50


# Vulnerability details

## Description

Cold signers can add call data checks as whitelisted checks that hot signers could execute without timelocks, the call data checks depend on the indices of the encoded call. However, the protocol invalidly handles these indices in 2 separate places:
1. https://github.com/code-423n4/2024-10-kleidi/blob/main/src/Timelock.sol#L1136
2. https://github.com/code-423n4/2024-10-kleidi/blob/main/src/BytesHelper.sol#L50

Where length is computed as `end index—start index`, which is usually wrong as index subtraction needs `+1` to be translated to a length. For most of the scenario, this is okay, however, if a parameter that is being checked filled all of its bytes then this would be an issue (PoC is an example), for example, a uint256 filling all of its 32 bytes.
**NB:** This is not caught in the unit tests because there isn't any test that checks this edge case, where a parameter that fills all its bytes is being checked.

This forces the whitelisted call to revert.

## Proof of Concept

The following PoC shows a scenario where an infinite approval call is being whitelisted, we don't want to allow fewer approvals (only uint256 max), so the encoding of the call:
```solidity
abi.encodeWithSelector(IERC20.approve.selector, owner, amount)
```
results in the following bytes string:
```solidity
0x095ea7b30000000000000000000000001eff47bc3a10a45d4b230b5d10e37751fe6aa718ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
```
To have an infinite approval call whitelisted we need to add conditions on both the spender and the amount:
1. Spender: `0x1efF47bc3a10a45D4B230B5d10E37751FE6AA718`
2. Amount: `115792089237316195423570985008687907853269984665640564039457584007913129639935`

For the spender part, it's straightforward where we need to check from index 16 to 35 (`1eff47bc3a10a45d4b230b5d10e37751fe6aa718` from the encoded bytes), however, passing 16 and 35 will cause the TX to revert with `CalldataList: Data length mismatch`, this is where the issue starts, we pass 16 to 36, but now 36 is the start of the unit max. And we pass 37 to 69, to have the whole 32 bytes included (unit max fills all 32 bytes), passing the end index less than 69 reverts.

Now, when the whitelisted call is triggered with the above params, the TX will revert with `End index is greater than the length of the byte string`, and this is because the amount's byte length is 68 while the end index is 69.

As a result: wrong index/length handling => forcing to pass incorrect params.

**Coded POC:**

Add the following test in `test/integration/System.t.sol`, and run it using `forge test -vv --fork-url "https://mainnet.infura.io/v3/PROJECT_ID" --fork-block-number 20515328 --mt test_DaiTransfer_withoutPlus1`:

```solidity
function test_DaiTransfer_withoutPlus1() public {
    ad
Recovery spells don't allow the addition or removal of owners, potentially leading to the compromisation of the wallet
Audit: 2024-10-kleidi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-10-kleidi/blob/main/src/RecoverySpell.sol#L165-L317


# Vulnerability details

## Description

When recovery spells are created, users preset the new owners of the safe that will be swapped. This is set at the wallet creation time, as the owners used will be part of the creation salt that is used to compute the recovery spell address. Recovery spells don't allow users to remove or add owners to that list.

In case 1 of the "new owners" is compromised and the recovery spell is used, that compromised owner will be injected as an owner in the safe's owners list, knowing that the previous owners are aware of this compromisation but had no solution to solve it.

This leads to the full compromisation of a wallet.

## Proof of Concept
1. User A predicts the safe address using `AddressCalculation::calculateAddress`.
2. User A uses the safe's predicted address to predict a recovery spell whose owners are users C and D.
3. User A creates the safe while passing the owners as users A and B and passing the recovery spell predicted address in step 2 as the recovery spell.
4. User A deploys the recovery spell.
**At this point we assume both users B and D are compromised**
5. User A initiates the wallet recovery by calling `executeRecovery` on the created spell, however, the issue here is that the new owners that will be replaced contain a compromised owner which is D.
6. At this point, user C can't directly remove D from the owners list in the save, as it'll require a time-locked transaction through the timelock, which is the same time user D will have to wait to execute a malicious transaction.

This results in a messed-up wallet, where a compromised owner is forcefully injected into the safe. As users before knew it was compromised but couldn't remove it.

## Recommended Mitigation Steps

Allow users to add/remove recovery spell owners, which can be done by other owners signing that transaction.



## Assessed type

Error
Intended configuration of `recoveryDelay < timelockDelay` allows for malicious takeover by recovery spells
Audit: 2024-10-kleidi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-10-kleidi/blob/ab89bcb443249e1524496b694ddb19e298dca799/src/RecoverySpell.sol#L95-L127


# Vulnerability details

# Vulnerability Details

## Impact

`RecoverySpell.sol` [states](https://github.com/code-423n4/2024-10-kleidi/blob/ab89bcb443249e1524496b694ddb19e298dca799/src/RecoverySpell.sol#L95-L99) that it is of CRITICAL importance for the deployer to have recovery spells with the `delay` for the `recoverySpell` to become executable (hereafter referred to as `recoveryDelay`) shorter than the `delay` for a scheduled proposal to become executable in `Timelock.sol` (hereafter referred to as `timelockDelay`)

Even though it is only a comment, it says that the condition should be met by default and is basically a rule that should be followed, but that it is not possible to enforce that in the smart contracts.

This will make deployers to follow the comment's instructions. However, if the recovery spell is malicious, they will lose their safe.


## Proof of Concept

1. The attacker creates a recovery spell with `recoveryDelay = 10 days`.

2. The deployer creates a new system instance with the malicious recovery spell and a `timelockDelay` of `20 days`. He chooses this delay because of the comment that says it is of critical importance for the following to be true: `recoveryDelay < timelockDelay`.

3. The `recoveryDelay` runs out and the malicious recovery spell gets executed.

## Recommended Mitigation Steps

The timing constraints recommended to be used by the protocol are unsafe and the protocol needs to assess which timing constraints are not exploitable, if such timing constraints exist.

It should also be assessed whether it is possible to enforce the timing constraints in the smart contracts.


## Assessed type

Timing
Missing Validation to Ensure Hot Signers are Safe Owners
Audit: 2024-10-kleidi-findings
Severity: MEDIUM

# Lines of code

https://github.com/solidity-labs-io/kleidi/blob/0d72b6cb5725c1380212dc76257da96fcfacf22f/src/Timelock.sol#L52
https://github.com/solidity-labs-io/kleidi/blob/0d72b6cb5725c1380212dc76257da96fcfacf22f/src/Timelock.sol#L264-L309
https://github.com/solidity-labs-io/kleidi/blob/0d72b6cb5725c1380212dc76257da96fcfacf22f/src/Timelock.sol#L759-L769


# Vulnerability details

## Description

In the `Timelock` contract, there is no check to ensure that the `hotSigners` are the same as the owners of the `safe`.

According to the comment on [#L:52](https://github.com/solidity-labs-io/kleidi/blob/0d72b6cb5725c1380212dc76257da96fcfacf22f/src/Timelock.sol#L52), "only safe owners can execute whitelisted calldatas," but the constructor and the `grantRole()` functions do not verify that hot signers are also safe owners.

The separation of the `owners` array for the safe contract and the `hotSigners` array for the timelock contract introduces the risk that non-safe owners could be granted the `HOT_SIGNER_ROLE` and have ablity to execute whitelisted calldatas.

This weakens the intended security model of the system by allowing external or unauthorized addresses to gain the ability to execute privileged actions.

## Impact

Non-safe owners may be granted the `HOT_SIGNER_ROLE`, enabling them to execute whitelisted calldatas without being authorized.

## Proof of Concepts

- In [Timelock constructor](https://github.com/solidity-labs-io/kleidi/blob/0d72b6cb5725c1380212dc76257da96fcfacf22f/src/Timelock.sol#L264-L309), hot signers are granted `HOT_SIGNER_ROLE`, without any checks to ensure they are also safe owners as it's stated in [#L:52](https://github.com/solidity-labs-io/kleidi/blob/0d72b6cb5725c1380212dc76257da96fcfacf22f/src/Timelock.sol#L52).

- the safe `owners` array and `hotSigners` array [are separate](https://github.com/solidity-labs-io/kleidi/blob/0d72b6cb5725c1380212dc76257da96fcfacf22f/src/TimelockFactory.sol#L6-L18), and nothing ensures in [TimelockFactory](https://github.com/solidity-labs-io/kleidi/blob/0d72b6cb5725c1380212dc76257da96fcfacf22f/src/TimelockFactory.sol#L36-L55) that `owners[i] == hotsigner[i]` when Deploying Timelock.

- The [grantRole()](https://github.com/solidity-labs-io/kleidi/blob/0d72b6cb5725c1380212dc76257da96fcfacf22f/src/Timelock.sol#L759-L769) function does not validate the `account` against the list of safe owners to ensure only safe owners can have `HOT_SIGNER_ROLE`, this allows an arbitrary address to be assigned `HOT_SIGNER_ROLE`, even if they may not be one of the safe owners.

## Recommended mitigation

- Add a check in the [Timelock constructor](https://github.com/solidity-labs-io/kleidi/blob/0d72b6cb5725c1380212dc76257da96fcfacf22f/src/Timelock.sol#L271C5-L309) to ensure that the account we are assigning `HOT_SIGNER_ROLE` to, is one of the safe owners.
  
- remove `hotSigners` array from [DeploymentParams](https://github.com/solidity-labs-io/kleidi/blob/0d72b6cb5725c1380212dc76257da96fcfacf22f/src/Timelock
UpdateExpirattionPeriod() cannot be execute when the newExpirationPeriod is less than currentExpirationPeriod.
Audit: 2024-10-kleidi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-10-kleidi/blob/ab89bcb443249e1524496b694ddb19e298dca799/src/Timelock.sol#L608
https://github.com/code-423n4/2024-10-kleidi/blob/ab89bcb443249e1524496b694ddb19e298dca799/src/Timelock.sol#L1009-L1015
https://github.com/code-423n4/2024-10-kleidi/blob/ab89bcb443249e1524496b694ddb19e298dca799/src/Timelock.sol#L399-L404


# Vulnerability details

## Description

Safe cannot reduce  `expirationPeriod` to a `newExpirationPeriod` when

    currentTimeStamp < timestamp[id] +  expirationPeriod and
    currentTimeStamp >= timestamp[id] +  newExpirationPeriod
    

 where `id` is the `hash` of `updateExpirationPeriod()` and `timestamp[id]` is the timestamp when the `id` can be executed.


Safe shuold be able to update the `expirationPeriod` to any values >= `MIN_DELAY` by scheduling the `updateExpirationPeriod()` and later execute from `timelock` when the operation is ready (before the expiry).

```solidity
    require(newPeriod >= MIN_DELAY, "Timelock: delay out of bounds");
```

But the protocol has overlooked the situation and added an reduntant  check inside [_afterCall()](https://github.com/code-423n4/2024-10-kleidi/blob/ab89bcb443249e1524496b694ddb19e298dca799/src/Timelock.sol#L1009-L1015) which is executed at the end of [_execute()](https://github.com/code-423n4/2024-10-kleidi/blob/ab89bcb443249e1524496b694ddb19e298dca799/src/Timelock.sol#L608).

```solidity
    function _afterCall(bytes32 id) private {
        /// unreachable state because removing the proposal id from the
        /// _liveProposals set prevents this function from being called on the
        /// same id twice
        require(isOperationReady(id), "Timelock: operation is not ready"); //@audit
        timestamps[id] = _DONE_TIMESTAMP;
    }
```

Here the `isOperationReady(id)` will be executed with the `newExpirationPeriod`.
[code](https://github.com/code-423n4/2024-10-kleidi/blob/ab89bcb443249e1524496b694ddb19e298dca799/src/Timelock.sol#L399-L404)
```solidity
    function isOperationReady(bytes32 id) public view returns (bool) {
        /// cache timestamp, save up to 2 extra SLOADs
        uint256 timestamp = timestamps[id];
        return timestamp > _DONE_TIMESTAMP && timestamp <= block.timestamp
   =>         && timestamp + expirationPeriod > block.timestamp;
    }
```
there it is checking whether the `currentTimestamp` is less than the `timestamp` + `updated EpirationPeriod` instead of the `actual expirationPeriod`.

## Proof Of Concept
forge test --match-test testDUpdateExpirationPeriodRevert -vvv

```solidity
function testDUpdateExpirationPeriodRevert() public {
        // Prepare the scheduling parameters
        // Call schedule() first time
        
        uint256 newExpirationPeriod =  EXPIRATION_PERIOD - 2 days; //newExpirationPeriod =  3 days since EXPIRATION_PERIOD = 5 days intially

        _schedule({       //safe has scheduled updateExpirationPeriod() call
            caller: address(safe),
            timelock: addr
The `_updatePauseDuration` function do not check if the contract is unpaused. And no check for if the contract was already paused, reset the pauseStartTime to 0
Audit: 2024-10-kleidi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Proof of Concept
function `Timelock.sol` is calling `_updatePauseDuration` with out checking if the contract already paused.
- [on this line](https://github.com/code-423n4/2024-10-kleidi/blob/main/src/Timelock.sol#L297)
- [also this line](https://github.com/code-423n4/2024-10-kleidi/blob/main/src/Timelock.sol#L987)

also `_updatePauseDuration` function do not have it's own check.

I pointed out the issues.
```solidity
    /// @notice helper function to update the pause duration
👉  /// should only be called when the contract is unpaused
    /// @param newPauseDuration new pause duration
    function _updatePauseDuration(uint128 newPauseDuration) internal {
        require(
            newPauseDuration >= MIN_PAUSE_DURATION
                && newPauseDuration <= MAX_PAUSE_DURATION,
            "ConfigurablePause: pause duration out of bounds"
        );

 👉     /// if the contract was already paused, reset the pauseStartTime to 0
        /// so that this function cannot pause the contract again
        _setPauseTime(0);

        uint256 oldPauseDuration = pauseDuration;
        pauseDuration = newPauseDuration;

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

[also on the line](https://github.com/code-423n4/2024-10-kleidi/blob/main/src/ConfigurablePause.sol#L111) it says if the contract was already paused, reset the pauseStartTime to 0.
so that this function cannot pause the contract again.

but it do not have any check to see if it is already paused not or. which lead it to be always set `_setPauseTime(0)` whatever condition is.


## Recommended Mitigation Steps
please update necessary steps.


## Assessed type

Invalid Validation