xanoutas/solidity-security-findings huggingface dataset:
DoS in pause function due to unbounded loop over proposals
Audit: 2024-10-kleidi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Proof of Concept

The vulnerability lies in the `pause()` function of the Timelock contract, where the contract is designed to be paused by the `pauseGuardian`. The issue arises because the `pause()` function contains a loop over all active proposals in `_liveProposals`. Each proposal requires gas to be canceled and removed from the set of active proposals. When the number of active proposals exceeds 9302, the gas required to execute the `pause()` function exceeds the block gas limit (30 million gas units), causing the transaction to fail due to out-of-gas errors.

Here’s a simplified explanation of the code involved:

- The `pause()` function begins by calling `super.pause()`, which ensures only the `pauseGuardian` can execute the function and marks the contract as paused.
- It then iterates over the `_liveProposals` set, deleting timestamps and removing each proposal one by one.
- If there are more than 9302 proposals, this process will consume more than the available block gas, resulting in a denial of service (DoS), as the `pause()` function will not be able to complete execution. As a result, the contract cannot be paused when it becomes necessary, potentially exposing it to further risks.

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

    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);
    }
}
```

The following test demonstrates the DoS condition when there are more than 9302 proposals.

To execute it, on the `\test\unit\Timelock.t.sol` file: 
1. add `import {console} from "forge-std/console.sol";` at the beggining of the file.
2. add the following function
```solidity
function testPausePOC() public {
    uint256 newDelay = MINIMUM_DELAY + 3 days;
    uint256 size = 9303;

    for (uint256 i = 0; i < size; i++) {
        _schedule({
            caller: address(safe),
            timelock: address(timelock),
            target: address(timelock),
            value: 0,
            data: abi.encodeWithSelector(timelock.updateDelay.selector, newDelay),
            salt: bytes32(abi.encode(i)),
            delay: MINIMUM_DELAY
        });
    }

    vm.prank(guardian);

    uint256 gasBefore = gasleft();
    timelock.pause();

    uint256 gas = gasBefore - gasleft();
    console.log(gas);

    assertTrue(timelock.paused(), "timelock should be paused");
}
```

When this test is executed using the command `forge test --match-test testPausePOC -vvv`, the output shows that the transaction consumes 30,001,690 gas, exceeding the block gas limit:

```
Logs:
  30001690
```

This demonstrates that the `pause
Timelock can take a contract off pause.
Audit: 2024-10-kleidi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Vulnerability details
`Timelock` can take a contract off pause. In natspec it is described that the function 
`_updatePauseDuration` *should only be called when the contract is unpaused*
but in the function body there is a function that unpauses the contract.

```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);
    }

```
## Impact
Suppose that `pauser` and `admin` are currently not communicating with each other. And in one block there are two transactions in which first pauser pauses the contract. And at this moment admin decides to update the pause duration. 
Depending on the purpose of the pause, vulnerability or something else, this will give time for at least **compromised** `HOT_WALLETS` to exploit this vulnerability, because in order to pause the contract again, a new pauser must first be appointed and only then must the paused contract be paused. It also gives the admin extra powers, which can have a bad effect on trust.
## Tools Used
Manual Review
## Recommended Mitigation Steps
It would be more logical to add the `whenNotPaused` modifier to the `updatePauseDuration` function and delete `_setPauseTime(0)`

```solidity 
function updatePauseDuration(

uint128 newPauseDuration

) external onlyTimelock whenNotPaused {

/// min and max checks are done in the internal function

_updatePauseDuration(newPauseDuration);

}
```

```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"
        );

        uint256 oldPauseDuration = pauseDuration;
        pauseDuration = newPauseDuration;

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


## Assessed type

Other
When safe's private key is leaked, the protocol cannot be protected by pause, and the attacker can prevent pause.
Audit: 2024-10-kleidi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Proof of Concept
background:
1. The protocol hopes to use pause to ensure the safety of funds when the safe wallet loses control. After the pause, the attacker cannot perform any operations through safe.
2. In the pause function, all proposals will be looped and deleted.
3. If an attacker obtains the safe wallet, [he can freely create proposals](https://github.com/code-423n4/2024-10-kleidi/blob/ab89bcb443249e1524496b694ddb19e298dca799/src/Timelock.sol#L521).

```solidity
        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);
        }
```

Therefore, an attacker can continue to create proposals after gaining control of the safe wallet. Let the number of proposals be huge. Until the pause function is called, it fails because the gas exceeds the gas limit of the transaction. This prevents pausing.

If we calculate the gas consumption of the pause function, we can get:
• Remove timestamps[id]: ~5,000 gas.
• Remove _liveProposals.remove(id): Assume ~15,000 gas.
• Trigger event emit Canceled(id): about 631 gas.
• Base cost for other memory operations, array accesses, and control flow: approximately 1,000 gas.
• The total gas consumption per loop is roughly: 5000 + 15000 + 631 + 1000 = 21,631 gas
Assuming that the gas limit of a single transaction is 15,000,000 gas, using the previously estimated gas of 21,631 gas per cycle, we can calculate the approximate number of cycles ≈ 15,000,000 / 21,631 ≈ 693 times

Although the attacker needs to pay some gas cost. But if the amount of timelock funds is huge, then the effort is worth it.

## Recommended Mitigation Steps
It is recommended not to include the operation of deleting proposals in pause. We can write another function for pause to call.


## Assessed type

DoS
Legitimate proposal might be unable to call `updateExpirationPeriod()` due to `_afterCall()` validation
Audit: 2024-10-kleidi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

# Vulnerability Details

## Impact

This issue is about how the scheduling and execution flow of the protocol can prevent a proposal to decrease the `expirationPeriod` from being executable, even though it has not yet expired.

This means that a safe has to schedule a new proposal and has to wait up to 30 days before it can get executed, which could revert again.

The fact that, when the contract is used as intended, it can lead to a user being unable to decrease the `expirationPeriod` and having to wait up to 30 days for the next proposal leads me to estimate this as a "Medium" severity issue.

## Proof of Concept

During the scheduling and execution flow of a proposal, the user has to consider two types of time periods:
1. `delay`, which represents the time a user has to wait for before anyone can execute it
2. `expirationPeriod`, which represents the timeperiod during which the proposal can be executed by anyone

Let's first look at the `_schedule(bytes32 id, uint256 delay)` function, which is wrapped by `schedule(address target, uint256 value, bytes calldata data, bytes32 salt, uint256 delay)`.

```solidity
function _schedule(bytes32 id, uint256 delay) private {
    /// this line is never reachable as no duplicate id's are enforced before this call is made
    require(!isOperation(id), "Timelock: operation already scheduled");
    /// this line is reachable
    require(delay >= minDelay, "Timelock: insufficient delay");
    timestamps[id] = block.timestamp + delay;
}
```
What is important is that the `timestamps[]` mapping saves the time after which anyone can execute the proposal.

Now we need to look at the `execute(address target,uint256 value, bytes calldata payload, bytes32 salt)` function in `Timelock.sol`, which is used to execute proposals that have been scheduled by the safe using `schedule(address target, uint256 value, bytes calldata data, bytes32 salt, uint256 delay)`.

```solidity
function execute(
    address target,
    uint256 value,
    bytes calldata payload,
    bytes32 salt
) external payable whenNotPaused {
    bytes32 id = hashOperation(target, value, payload, salt);

    /// first reentrancy check, impossible to reenter and execute the same
    /// proposal twice
    require(_liveProposals.remove(id), "Timelock: proposal does not exist");
    require(isOperationReady(id), "Timelock: operation is not ready");
    _execute(target, value, payload);
    emit CallExecuted(id, 0, target, value, payload);

    /// sec
Potential Denial-of-Service (DoS) in Timelock `pause` Function
Audit: 2024-10-kleidi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Proof of Concept
The current implementation of the [`pause`](https://github.com/code-423n4/2024-10-kleidi/blob/ab89bcb443249e1524496b694ddb19e298dca799/src/Timelock.sol#L687-L698) function attempts to cancel all live proposals within a single transaction:
```solidity
function pause() public override {
    super.pause();

    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);
    }
}
```
This implementation is vulnerable to a DoS attack in the following scenario:
1. An attacker creates numerous small proposals, inflating the `_liveProposals` set.
2. The number of proposals becomes so large that the gas required to process all of them in the `pause` function exceeds the block gas limit.
As a result, it becomes impossible to pause the contract, potentially leaving it in a vulnerable state during emergencies

## Recommended Mitigation Steps
Separate the pausing mechanism from the proposal cancellation process


## Assessed type

DoS
Potential DoS Due to Retrieving Large Number of Proposals or Calldatas in Functions that Modify State 
Audit: 2024-10-kleidi-findings
Severity: MEDIUM

# Lines of code

https://github.com/solidity-labs-io/kleidi/blob/0d72b6cb5725c1380212dc76257da96fcfacf22f/src/Timelock.sol#L685-L700
https://github.com/solidity-labs-io/kleidi/blob/0d72b6cb5725c1380212dc76257da96fcfacf22f/src/Timelock.sol#L871-L934
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#L1245-L1286


# Vulnerability details


## Description

The use of `Bytes32Set.values()` in multiple state-changing functions could lead to a denial of service (DoS).

This operation copies the entire storage to memory, which is costly, especially when the set grows large.

Since it can become prohibitively expensive to call functions like `pause()`, `_addCalldataCheck()`, `removeCalldataCheckDatahash()`, `_removeCalldataCheck()` and `_removeAllCalldataChecks()`, it may render them uncallable.

This could result in situations where the pause guardian doesn't desire to pause the contract, or the timelock refuses/is-unable to manage calldatas due to high-gas-costs/exceeding-block-gaslimit.

Without setting a proper limits on the number of live proposals timelock can have or calldatas that can be stored per index range, there's possiblity of Denial of Service, that can happen.

Also we can take a look at function natspec of `values(Bytes32Set storage set)` inside the `EnumerableSet.sol`, where OZ has warned us about it: [#L211-L228](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/28aed34dc5e025e61ea0390c18cac875bfde1a78/contracts/utils/structs/EnumerableSet.sol#L211-L228)

```javascript
    /**
     * @dev Return the entire set in an array
     *
     * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
     * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
     * this function has an unbounded cost, and using it as part of a state-changing function may render the function
     * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
     */
    function values(Bytes32Set storage set) internal view returns (bytes32[] memory) {
        bytes32[] memory store = _values(set._inner);
        bytes32[] memory result;

        assembly ("memory-safe") {
            result := store
        }

        return result;
    }

```

## Impact

Expensive gas operations could prevent critical timelock functions from being executed. like pause guardian refusing/notHavingEnoughFunds to pause the contract due to very high gas cost incase of emergency or timelock refusing/is-unable to manage calldatas due to high-gas-cost/exceeding-block-gas-limit.

## Proof Of Concept

`Bytes32Set.values()` is used in funct
Dangerous Implementation of Wildcard Functionality Can Lead to Unexpected Critical Incidents In Some Scenarios
Audit: 2024-10-kleidi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Description

The `addCheckCalldata()` function in the `Timelock` contract, currently allows for the entire calldata to be arbitrary if wildcard is set. This poses a security risk, as malicious hot signers could take advantage of the wildcard for arbitrary function calls with harmful parameters in some scenarios.

instead the wildcard functionality should be implemented in a way that, Timelock allows certain parts of calldata related to a specific contract and function, to be arbitrary(wildcard) and certain parts of calldata have restrictions(whitelisted).

its true that safe owners should be extra cautious when choosing a function for granting wildcard to, but the way the wildcard functionality is implemented now, it simply allows the hot signer to input any arbitrary data to each parameter and there's no way for safe owners to grant a wildcard to only specific parameter of a function.

also seems like developer wanted to implement the wildcard functionality the way i described because if we take a look at [checkCalldata()](https://github.com/solidity-labs-io/kleidi/blob/0d72b6cb5725c1380212dc76257da96fcfacf22f/src/Timelock.sol#L470-L504) function, specifically [this part](https://github.com/solidity-labs-io/kleidi/blob/0d72b6cb5725c1380212dc76257da96fcfacf22f/src/Timelock.sol#L488-L493). we check if retrieved index has granted a wildcard or not, but developer hasn't noticed that he implemented wildcard functionality in a way that once a function is granted a wildcard, no more indexes can be added for that function because of [this part](https://github.com/solidity-labs-io/kleidi/blob/0d72b6cb5725c1380212dc76257da96fcfacf22f/src/Timelock.sol#L1062-L1067) in [_addCalldataCheck()](https://github.com/solidity-labs-io/kleidi/blob/0d72b6cb5725c1380212dc76257da96fcfacf22f/src/Timelock.sol#L1028-L1155).

## Impact

Once a function is granted wildcard access, it gives hot signers too much ability to act freely or even maliciously in some scenarios, if wildcard is granted to wrong function. This violates the principle of least privilege in security, enabling malicious or unexpected actions.

it's better that the wildcard functionality is implemented in such way, that if safe owners decided to grant wildcard access to specific part of calldata, they could, instead of allowing the whole calldata to arbitrary. also be able to restrict certain parts of calldata with whitelisting calldatas.

## Proof Of Concept

wildcard functionality is currently implemented this way:

* we can determine whether or not wildcard is set for a function, by checking first index struct of a function. specifically it's `startIndex` and `endIndex` values.
  if (`startIndex == endIndex` && `startIndex == 4` && `_calldataList[contractAddress][selector].length == 1`), it means wildcard is granted for that function. you can
`addCalldataCheck()` will always revert when more than 1 parameter is whitelisted for the callDatas.
Audit: 2024-10-kleidi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Proof of Concept

Due to an invalid check of `startIndex` and `endIndex`, parameters for the functionSelectors cannot be added to the `callDataCheck.dataHash` correctly.

lets say ,  safe wants to whiteList `mul2numbers(bytes2 x,bytes2 y)` from a contract `Multiply` so that hot signers can execute mul operations from `timelock`.

safe will schedule an operation to add this `callDatacheck` so that when the operation is ready to execute, `timeLock` can execute `addCallDataCheck` (a user will be callling execute() to execute this operation).

```solidity
timelock.addCalldataCheck(
        contractAddress, selector, startIndex, endIndex, datas
);
```

Now to whitelist a `bytes2` value `(hex("1234"))` for the parameter `x` , the inputs datas passed would be
```
    contractAddress : address(`Multiply`)
    selector : timelock.addCalldataCheck.selector,
    startIndex:4
    endIndex:6
    datas:hex("1234")
```

and then safe decided to whitelist another bytes2 value (hex("1234")) for the parameter `y` , here the inputs datas passed would be
```
    contractAddress : address(`Multiply`)
    selector : timelock.addCalldataCheck.selector,
    startIndex:6
    endIndex:8
    datas:hex("3456")
```
This should efficiently store both these parameters inside the callDataChecks since

   ` hex(1234)` will occupy the `callDatas`'s  ` 4th index` and `5th index` (total occupying 2 bytes spaces) and
    `hex(3456)` will occupy the `callDatas`'s  `6th index` and `8th index` (total occupying 2 bytes spaces)

>There shouldnot not be any overlap.

But,
due to the [check](https://github.com/code-423n4/2024-10-kleidi/blob/ab89bcb443249e1524496b694ddb19e298dca799/src/Timelock.sol#L1119-L1123) in the `_addCalldataCheck()` 

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

>`startIndex` can equal to `endIndex` of newly added callDataCheck since while adding a callData value into a callDataCheck , it will only occupy from `startIndex` to `endIndex - 1` of it.


### POC
forge test --match-test testDCantAddDatastoCallDataCheck -vvv

```solidity
    function testDCantAddDatastoCallDataCheck() public {
        bytes[] memory datasX = new bytes[](1);
        datasX[0] = hex"1234"; //it is a 2 bytes long data

        vm.prank(address(timelock));
        timelock.addCalldataCheck(
            address(10000), timelock.addCalldataCheck.selector, 4, 6, datasX
        ); //occupy 4th and 5th index

        bytes[] memory datasY = new bytes[](1);
        datasY[0] = hex"3456"; //it is a 2 bytes long data
        vm.prank(address(timelock));
        vm.expectRevert("CalldataList: Partial check overlap"); // It will revert with this message 
        timelock.addCalld
Missing whenNotPaused Check in updatePauseDuration Function May Lead to Inconsistent Behavior
Audit: 2024-10-kleidi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-10-kleidi/blob/ab89bcb443249e1524496b694ddb19e298dca799/src/Timelock.sol#L982-L988
https://github.com/code-423n4/2024-10-kleidi/blob/ab89bcb443249e1524496b694ddb19e298dca799/src/ConfigurablePause.sol#L101-L105


# Vulnerability details

## Proof of Concept

The function `updatePauseDuration()` lacks `whenNotPaused` modifier check.

https://github.com/code-423n4/2024-10-kleidi/blob/ab89bcb443249e1524496b694ddb19e298dca799/src/Timelock.sol#L982-L988
```solidity
    function updatePauseDuration(uint128 newPauseDuration)
        external
        onlyTimelock /*@audit-issue should be able to call only when its unpaused (missing whenNotPaused check).*/
    {
        /// min and max checks are done in the internal function
                _updatePauseDuration(newPauseDuration);
    }
```

The `pauseDuration` are supposed to be updated only when the `timelock` is unpaused.

https://github.com/code-423n4/2024-10-kleidi/blob/ab89bcb443249e1524496b694ddb19e298dca799/src/ConfigurablePause.sol#L101-L105
```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 {
       
    }
```

## Recommended Mitigation Steps

include whenNotPaused check.


## Assessed type

Other