xanoutas/solidity-security-findings huggingface dataset:
Missing access control in `create_pool_D650_E2_D0()` allows for unauthorized pool creation
Audit: 2024-10-superposition-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-10-superposition/blob/7ad51104a8514d46e5c3d756264564426f2927fe/pkg/seawater/src/lib.rs#L120-L121
https://github.com/code-423n4/2024-10-superposition/blob/7ad51104a8514d46e5c3d756264564426f2927fe/pkg/seawater/src/lib.rs#L802-L834


# Vulnerability details

## Proof of Concept
The [following](https://github.com/code-423n4/2024-10-superposition/blob/7ad51104a8514d46e5c3d756264564426f2927fe/pkg/seawater/src/lib.rs#L120-L121) is given:
```solidity
    // authorised enablers to create new pools, and enable them
    authorised_enablers: StorageMap<Address, StorageBool>,
```
However, the [`create_pool_D650_E2_D0()`](https://github.com/code-423n4/2024-10-superposition/blob/7ad51104a8514d46e5c3d756264564426f2927fe/pkg/seawater/src/lib.rs#L802-L834) function does not check if the caller has the necessary permissions, meaning any actor could potentially create a pool, leading to unauthorized pool creation.
```solidity
    pub fn create_pool_D650_E2_D0(
        &mut self,
        pool: Address,
        price: U256,
        fee: u32,
    ) -> Result<(), Revert> {
        
        let tick_spacing = match fee {
            0 => Ok(1),
            500 => Ok(10),
            3000 => Ok(60),
            10_000 => Ok(200),
            _ => Err(Error::BadFee)
        }?;

        let max_liq_per_tick = tick_math::tick_spacing_to_max_liq(tick_spacing)?;

        self.pools
            .setter(pool)
            .init(price, fee, tick_spacing, max_liq_per_tick)?; 

        // Get the decimals for the asset so we can log its decimals for the indexer
        let _decimals = erc20::decimals(pool)?;

        evm::log(events::NewPool {
            token: pool,
            fee,
            decimals: _decimals,
            tickSpacing: tick_spacing,
        });

        Ok(())
    }
```
The function lacks a check against the `authorised_enablers` map, which is intended to restrict pool creation to authorized addresses.


## Impact
This could result in unauthorized or malicious pool creations. Since no verification is in place, it exposes the contract to potential misuse, such as the creation of pools with invalid parameters or for tokens not intended to be supported by the system.

## Recommended Mitigation Steps
Introduce an access control check at the start of the function to verify if the caller is authorized.
```diff
+       assert_or!(
+           self.authorised_enablers.get(msg::sender()),
+           Error::Unauthorized
+       );
```






## Assessed type

Access Control
Incorrect slippage handling in `swap_internal()`
Audit: 2024-10-superposition-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-10-superposition/blob/7ad51104a8514d46e5c3d756264564426f2927fe/pkg/seawater/src/lib.rs#L183-L186


# Vulnerability details

## Proof of Concept
In the [`swap_internal()`](https://github.com/code-423n4/2024-10-superposition/blob/7ad51104a8514d46e5c3d756264564426f2927fe/pkg/seawater/src/lib.rs#L183-L186) function, the slippage check uses the `||` operator to validate the swap results. This can lead to a scenario where one of the amounts (`amount_0_abs` or `amount_1_abs`) is allowed to be zero, potentially resulting in unwanted `slippage`.
```solidity
    assert_or!(
        amount_0_abs > U256::zero() || amount_1_abs > U256::zero(), // Problematic operator
        Error::SwapResultTooLow
    );
```
Using the `||` operator allows the swap to proceed even if one of the amounts is `zero`, which could lead to unacceptable slippage.

**Scenario**:
- Consider a user swapping `100` units of token A (`amount_0`) for token B (`amount_1`). 
- Due to slippage, `token B`’s output (`amount_1_abs`) becomes `zero`, while token A’s output (`amount_0_abs`) remains positive. 
- With the current `||` operator, the `swap` would still be considered `valid` since one amount is greater than zero, even though the user receives no token B (`amount_1_abs = 0`), resulting in a poor outcome for the user.

## Impact
Using the `||` operator means that one token amount can be zero while the other passes the check, leading to an imbalanced swap that might not meet user expectations.

## Recommended Mitigation Steps
Replace the `||` operator with the `&&` operator to ensure both token amounts are greater than `zero`.
```diff
    assert_or!(
-       amount_0_abs > U256::zero() || amount_1_abs > U256::zero(),
+       amount_0_abs > U256::zero() && amount_1_abs > U256::zero(),
        Error::SwapResultTooLow
    );
```



## Assessed type

Invalid Validation
Users can add liquidity to disabled pools
Audit: 2024-10-superposition-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-10-superposition/blob/main/pkg/seawater/src/pool.rs#L86


# Vulnerability details

## Impact
Disabling a pool won't have any effects, as users can still add and remove liquidity.

## Proof of Concept
The previous issue wasn't fixed properly:
https://github.com/code-423n4/2024-08-superposition-findings/issues/31

With the current code, users can add liquidity even if the pool is disabled, but they should be able only to remove liquidity when the pool is not enabled:
https://github.com/code-423n4/2024-10-superposition/blob/main/pkg/seawater/src/pool.rs#L86

They can do so by calling:
`incr_position_E_2437399 > adjust_position_internal > adjust_position > update_position`

as none of these functions will revert if the pool is disabled.


## Recommended Mitigation Steps
Consider reimplementing the previous check, but only when the delta is positive:

```diff
    pub fn update_position(&mut self, id: U256, delta: i128) -> Result<(I256, I256), Revert> {
+       if delta > 0 {
+           assert_or!(self.enabled.get(), Error::PoolDisabled);
+       }
        let position = self.positions.positions.get(id);
        let lower = position.lower.get().sys();
        let upper = position.upper.get().sys();

```



## Assessed type

Invalid Validation
`getApproved` wrongly checks the approved address instead of the owner
Audit: 2024-10-superposition-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-10-superposition/blob/7ad51104a8514d46e5c3d756264564426f2927fe/pkg/sol/OwnershipNFTs.sol#L59


# Vulnerability details

## Impact
`getApproved` throws instead of returning the zero address when a token is not approved, breaking EIP721 compatibility.

## Proof of Concept

`getApproved` checks the approved address instead of the owner:

```solidity
    function getApproved(uint256 _tokenId) external view returns (address) {
        address approved = getApproved_[_tokenId];
        require(approved != address(0), "not existing"); //@audit-issue should check owner, not approved
        return approved;
    }
```

But it should check that instead:
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol#L115

## Recommended Mitigation Steps

Consider the following change:

```diff
    function getApproved(uint256 _tokenId) external view returns (address) {
        address approved = getApproved_[_tokenId];
-       require(approved != address(0), "not existing");
+       require(ownerOf(_tokenId) != address(0), "not existing");
        return approved;
    }
```
Missing Pause Time Checks in `_updatePauseDuration` Function
Audit: 2024-10-kleidi-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Proof of Concept
In the current `ConfigurablePause` contract, the function [`_updatePauseDuration`](https://github.com/code-423n4/2024-10-kleidi/blob/ab89bcb443249e1524496b694ddb19e298dca799/src/ConfigurablePause.sol#L104-L119) allows the modification of the `pauseDuration` value. However, this function does not check if the contract is in a paused state when updating the pause duration, which could lead to issues if the contract is already paused.

Here’s the current implementation of `_updatePauseDuration`:
```solidity
    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);
    }
```
The issue here is that `_updatePauseDuration` doesn't check if the contract is currently paused before setting a new `pauseDuration`. As a result, it resets the `pauseStartTime` to `0`, even if the contract is already paused, which could lead to the contract unintentionally becoming unpaused.

Scenario
Assume the contract is in a paused state (`pauseStartTime != 0`). The function `_updatePauseDuration` is called, setting `pauseStartTime` to 0. This causes the contract to effectively become unpaused, even though it was previously paused, without considering the pause window or current state.
This can result in a situation where the contract behaves as unpaused even though it was intended to remain paused, thus bypassing the restrictions meant to be enforced during the pause period.

## Recommended Mitigation Steps
To avoid unintentionally resetting the pause state when the contract is already paused, add a check to ensure that the contract is not paused before updating the pause duration. This will maintain the integrity of the pause state and ensure that the contract does not inadvertently unpause itself.
```diff
+   function _updatePauseDuration(uint128 newPauseDuration) internal whenNotPaused { 
        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;
Missing New Recovery Spell Replacement in the Recovery Process, To Ensure Safe Can Be Saved Again
Audit: 2024-10-kleidi-findings
Severity: HIGH

# Lines of code

https://github.com/solidity-labs-io/kleidi/blob/0d72b6cb5725c1380212dc76257da96fcfacf22f/src/RecoverySpell.sol#L154-L317


# Vulnerability details


## Description

The `RecoverySpell::executeRecovery()` function in the `RecoverySpell` contract is designed to recover the safe by replacing recovery spell owners with safe owners that lost keys.

However, once this recovery process is executed, the recovery spell is effectively a `one-time-use` operation, we will leaving no backup recovery mechanism in place for the safe.

If new owners of safe lose their keys again after recovery and safe has no other recovery spells enabled as module(backup plan), owners of safe will be unable to recover their safe again.

This lack of a fallback could result in new safe owners being unable to use safe forever and interact with `Timelock` and most likely losing funds stored inside the Timelock, if the `RecoverySpell` used to recover the safe, was their last recovery plan.

## Impact

In a situations where single `RecoverySpell` contract is the last recovery mechanism set in place for the safe, if the keys are lost again after a successful recovery, the new owners will have no way to regain access to safe again. 

This creates a critical situation where we have inablity to restore our funds from Timelock since We can't access Safe to Schedule Operations.

## Proof Of Concept

Lets say our safe has only single Recovery Method Available. after using it and recovering our safe, we will leaving our safe with no backup plan, meaning if we forget to set new `RecoverySpell` as safe module, there's no way to recover our safe if new owners lost private keys again.

so for that reason it is critical to enforce owners of `RecoverySpell`, to add replacement as recovery method, in safe recovery process.

## Recommended Mitigation

1. add the following function inside to `RecoverySpell.sol`:


```javascript
    /*
    @notice this function is gonna be used to ensure the `newRecoverySpell`
            is valid contract address, not eq to ADDRESS_ZERO and is actually a
            Recovery Spell contract.
    */
    function isRecoverySpell() public pure returns(bool) {
        return true
    }
```


2. add a new address parameter called `newRecoverySpell` to the `executeRecovery()` function. This parameter should take the address of a new recovery spell, which will be added as a safe module during the recovery process. This will ensure that a replacement recovery mechanism is set up when the current spell is used. The modified process could look like this:

```diff
    function executeRecovery(
        address previousModule,
+       address newRecoverySpell
        uint8[] calldata v,
        bytes32[] calldata r,
        bytes32[] calldata s
    ) external {
        
+       require(newRecoverySpell != address(this) && newRecoverySpell != address(0)); // ensure new Recovery Spell is not the same contract we are initiating recovery and is valid address
+
The same address in original owner set and recovery owner set can lead to permanent DoS
Audit: 2024-10-kleidi-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Describtion
The contract `RecoverySpell` is deployed with an array of new owners. This array is used when recovery is executed and new owners are set. However there is an edge case scenario which causes revert of the function `executeRecovery`.

#### Example scenario:
We have 3/4 multisig. And one of the signers is another multisig. 
This wallet is considered safe and thats why we include the wallet to the recovery owners array. Other addresses will be new.

Threshold is set to 3/4 and 2 signer keys are lost. So we have the following list of Safe owners

```
# L - Lost key
# M - multisig
og = [A_L, B_L, C, D_M]
```

and a set of new recovery owners:

```
new = [D_M, E, F, G]
```

And we execute recovery process.

Now if we follow the code
* _remove all but final existing owner, set owner threshold to 1_

state after `OwnerManager.removeOwner` calls
```
og = [D_M]
```

* _swap final existing owner for the first new owner_

and **here** is the problem. Swapping `[existingOwners[existingOwnersLength - 1]` for `owners[0]`
Aka **Swapping D_M for D_M**.

We can see in Safe `OwnerManager` contract:

```solidity
    function swapOwner(address prevOwner, address oldOwner, address newOwner) public authorized {
        // Owner address cannot be null, the sentinel or the Safe itself.
        require(newOwner != address(0) && newOwner != SENTINEL_OWNERS && newOwner != address(this), "GS203");
        // No duplicate owners allowed.
        require(owners[newOwner] == address(0), "GS204");
        // Validate oldOwner address and check that it corresponds to owner index.
        require(oldOwner != address(0) && oldOwner != SENTINEL_OWNERS, "GS203");
        require(owners[prevOwner] == oldOwner, "GS205");
        owners[newOwner] = owners[oldOwner];
        owners[prevOwner] = newOwner;
        owners[oldOwner] = address(0);
        emit RemovedOwner(oldOwner);
        emit AddedOwner(newOwner);
    }
```

More specifically:

```solidity
require(owners[newOwner] == address(0), "GS204");
```

The address `D_M` was not removed before, and it is the last address remaining. It does point to `SENTINEL_OWNERS`.

It is an edge scenario, but the impact is very dangerous as it will make the recovery permanently DoSed. The system should be robust enough for all different use-cases, including the mentioned one.

## PoC

See Safe test - same address swap fail:
https://github.com/safe-global/safe-smart-account/blob/6fde75d29c8b52d5ac0c93a6fb7631d434b64119/test/core/Safe.OwnerManager.spec.ts#L313C1-L322C12

## Recommended Mitigation Steps
In case any address will be re-used for a new owner set, make sure it does not occupy the zero index of recovery owners


## Assessed type

Invalid Validation
`executeRecoverySpell()` can revert due to check in `OwnerManager.swapOwner()` and causes safe to be compromised
Audit: 2024-10-kleidi-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-10-kleidi/blob/ab89bcb443249e1524496b694ddb19e298dca799/src/RecoverySpell.sol#L165-L317
https://github.com/safe-global/safe-smart-account/blob/bf943f80fec5ac647159d26161446ac5d716a294/contracts/base/OwnerManager.sol#L99-L112


# Vulnerability details

# Vulnerability Details

## Impact

The reason why recovery spells exist is to make sure that when the cold signers are compromised, it is possible to rotate the signing keys.

Therefore, when `executeRecoverySpell()` is called, the existing owners will be deleted and the new owners will be added.

As the safe does not allow removing all owners (threshold must be >= 1), the last existing owner will be swapped with the first new owner.

However, if these two owners have the same address, it will revert.

This means that the recovery spell to rotate the signing keys can not be executed and the safe with all its funds will be lost.

It could be possible to think that this is not an issue, as no recovery owner should be an already active owner, but this assumption is not true because of the following use case:

Imagine one cold signer key is held by a bank and the wallet is set up in a way that you are only allowed to rotate the signing keys if you keep the signing key held by the bank as a owner.
The signing key of the bank is thus assumed to be uncompromisable.

Keeping this key will cause `executeRecoverySpell()` to revert, if the address is at the first index of the current owner array and at the last index of the new owner array.

## Proof of Concept

The following test should be added in `RecoverySpells.t.sol`:

```solidity
function testRecoverySpellSameOwnerRevert()
    public
    returns (RecoverySpell recovery)
{
    address[] memory newRecoveryOwners = new address[](1);
    newRecoveryOwners[0] = vm.addr(pk3);

    NewInstance memory instance = NewInstance(
        owners,
        QUORUM,
        /// no recovery spells for now
        new address[](0),
        DeploymentParams(
            MINIMUM_DELAY,
            EXPIRATION_PERIOD,
            guardian,
            PAUSE_DURATION + 1,
            hotSigners,
            new address[](0),
            new bytes4[](0),
            new uint16[](0),
            new uint16[](0),
            new bytes[][](0),
            bytes32(0)
        )
    );

    SystemInstance memory calculatedInstance =
        addressCalculation.calculateAddress(instance);

    address newRecoverySpellAddress = recoveryFactory.calculateAddress(
        recoverySalt,
        newRecoveryOwners,
        address(calculatedInstance.safe),
        1,
        1,
        recoveryDelay
    );

    address[] memory recoverySpells = new address[](1);
    recoverySpells[0] = newRecoverySpellAddress;
    instance.recoverySpells = recoverySpells;

    vm.prank(HOT_SIGNER_ONE);
    SystemInstance memory walletInstance =
        deployer.createSystemInstance(instance);

        
        
    recovery = recoveryFactory.createRecoverySpe
Immutable Recovery threshold limits flexibility, risking loss of Safe access in critical scenarios
Audit: 2024-10-kleidi-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Summary
The `RecoverySpell` contract is a critical component of the Kleidi system, intended to enable secure recovery of Safe accounts in the event that an owner loses access to their private key. However, the contract’s design makes the recovery threshold immutable, meaning it cannot be adjusted once set. This inflexibility poses a risk: if one of the recovery participants also loses their access key, it becomes impossible to meet the threshold required for recovery. This design flaw undermines one of the primary functions of the Kleidi system, potentially locking users out of their funds.

## Proof of Concept
In `RecoverySpell`, the [`threshold`](https://github.com/code-423n4/2024-10-kleidi/blob/ab89bcb443249e1524496b694ddb19e298dca799/src/RecoverySpell.sol#L51) and [`recoveryThreshold`](https://github.com/code-423n4/2024-10-kleidi/blob/ab89bcb443249e1524496b694ddb19e298dca799/src/RecoverySpell.sol#L56) values are defined as immutable, meaning they are assigned at deployment and cannot be modified later. If an owner (Joe) creates a `RecoverySpell` module with a threshold based on his wife, son, and friend as authorized participants, but later, one of these participants also loses their key, recovery becomes impossible due to the fixed threshold. This inability to adapt in emergencies prevents Joe from recovering his Safe, ultimately blocking access to his funds forever.

## Impact
The inability to adjust recovery thresholds results in a critical loss of flexibility in emergency scenarios. If a recovery participant loses their key, the immutability of the threshold blocks the entire recovery process. This can effectively render the Safe unrecoverable, exposing users to potential loss of funds, particularly in multi-participant recovery setups.

## Recommended Mitigation Steps
Introduce a mechanism to adjust the recovery threshold dynamically if needed in emergency cases. One approach would be to allow a majority of participants (e.g., `recoveryAddressArr.length - 1`) to approve a reset of the threshold under special conditions. This would maintain security while providing critical flexibility for recovery in cases where one or more participants are unavailable.


## Assessed type

Context