xanoutas/solidity-security-findings huggingface dataset:
Any address (including an EUA) can be registered as a wallet and register Sub Accounts which may result in unwanted behavior and a security breach.
Audit: 2023-10-brahma-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-10-brahma/blob/dd0b41031b199a0aa214e50758943712f9f574a0/contracts/src/core/registries/WalletRegistry.sol#L35


# Vulnerability details

It is possible to register any address (including an EUA) as a wallet and deploy a sub account through it, even though doing so is semantically wrong (only Console Accounts should own Sub Accounts). A user might do so by mistake (i.e. mistakingly thinking the creator of the Console Account needs to deploy the sub account) thus creating a security breach where the EUA account can run any transaction on the sub account without any policy or multisig validations. This will be made possible because during deployment the owner account is registered as a module of the sub-account, granting it unlimited permission to run transactions on the sub account Safe.

## POC
Add the following import and test function to the ExecuteTransaction.s.sol test file (make it the first test after setup)
```
import {ERC20} from "solady/tokens/ERC20.sol";

function testRegisterEUAOwner() public {
        address user = makeAddr("user");
        vm.startPrank(user);

        address[] memory owners = new address[](1);
        owners[0] = address(this);

        //user creates a console account
        IGnosisSafe consoleAcc = IGnosisSafe(safeDeployer.deployConsoleAccount(owners, 1, commit, keccak256("salt")));

        //user mistakingly registers itself as the owner of a sub account instead of the console doing so.
        walletRegistry.registerWallet();
        IGnosisSafe subAccount = IGnosisSafe(safeDeployer.deploySubAccount(owners, 1, commit, keccak256("salt")));

        //fund subaccount with USDC
         address USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
         uint256 usdcAmount = 1000000 * 70000;
        deal(USDC, address(subAccount), usdcAmount);

        //user account can take all sub safe USDC
        uint balanceBefore = ERC20(USDC).balanceOf(user);                                     
        subAccount.execTransactionFromModule(USDC,
                                             0, 
                                             abi.encodeCall(ERC20.transfer,(user,usdcAmount)),
                                             Enum.Operation.Call);

        uint balanceAfter = ERC20(USDC).balanceOf(user);                                     
        assertEq(balanceAfter-balanceBefore,usdcAmount);
     }
```

## Recommended Mitigation Steps

In the [WalletRegistry::RegisterWallet](https://github.com/code-423n4/2023-10-brahma/blob/dd0b41031b199a0aa214e50758943712f9f574a0/contracts/src/core/registries/WalletRegistry.sol#L35) function, add a check that the caller is a gnosis safe, for example using the following line at the start of the function (reverts if address not a safe):
```
 IGnosisSafe(msg.sender).nonce();
```




## Assessed type

Other
`ConsoleFallbackHandler.simulate()` function calls the delegatecall, may cause DoS
Audit: 2023-10-brahma-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-10-brahma/blob/main/contracts/src/core/ConsoleFallbackHandler.sol#L104


# Vulnerability details

## Impact

`ConsoleFallbackHandler.simulate()` function calls the delegatecall, may cause DoS

## Proof of Concept

According to the define, the `simulate()` performs a delegetecall on a targetContract in the context of self. However, it does not check the `targetContract`. If attacker uses a fake `targetContract` and set a function calling `selfdestruct`, it may cause contract being unuseful.
```solidity
/**
     * @dev Performs a delegetecall on a targetContract in the context of self.
     * Internally reverts execution to avoid side effects (making it static). Catches revert and returns encoded result as bytes.
     * @param targetContract Address of the contract containing the code to execute.
     * @param calldataPayload Calldata that should be sent to the target contract (encoded method name and arguments).
     */
    function simulate(address targetContract, bytes calldata calldataPayload)
        external
        returns (bytes memory response)
    {
```

## Tools Used

manual

## Recommended Mitigation Steps

Adding whitelist to limit `targetContract`


## Assessed type

call/delegatecall
Malicious actor can deploy backdoored `SubAccount`s imitating real `SubAccount`s
Audit: 2023-10-brahma-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-10-brahma/blob/main/contracts/src/core/SafeDeployer.sol#L82-L103
https://github.com/code-423n4/2023-10-brahma/blob/main/contracts/lib/safe-contracts/contracts/base/ModuleManager.sol#L32-L40


# Vulnerability details


## Impact
Lost user funds IF a `Console` starts using the backdoored `SubAccount` 

## Description
It's possible to deploy a "fake" [`SubAccount`](https://github.com/code-423n4/2023-10-brahma/blob/main/contracts/src/core/SafeDeployer.sol#L82-L103) that mimics a "real" [`SubAccount`](https://github.com/code-423n4/2023-10-brahma/blob/main/contracts/src/core/SafeDeployer.sol#L82-L103) that is connected to a "real" [`Console`](https://github.com/code-423n4/2023-10-brahma/blob/main/contracts/src/core/SafeDeployer.sol#L56-L71): has the same `owners`, same `threshold`, same `policyCommitment` and most importantly the same [`Console`](https://github.com/code-423n4/2023-10-brahma/blob/main/contracts/src/core/SafeDeployer.sol#L56-L71) as a module is allowed to operate on it. The trick of the attacker lies in enabling his own address as well as a `module` to operate on the [`SubAccount`](https://github.com/code-423n4/2023-10-brahma/blob/main/contracts/src/core/SafeDeployer.sol#L82-L103). 

Since the similarities the "real" [`Console`](https://github.com/code-423n4/2023-10-brahma/blob/main/contracts/src/core/SafeDeployer.sol#L56-L71) operators might start to use the "fake" [`SubAccount`](https://github.com/code-423n4/2023-10-brahma/blob/main/contracts/src/core/SafeDeployer.sol#L82-L103) and transfer funds to it. When this happens the attacker can drain the [`SubAccount`](https://github.com/code-423n4/2023-10-brahma/blob/main/contracts/src/core/SafeDeployer.sol#L82-L103) 's funds completely (and could take control of the whole `Safe`).

If a `Console` is already deployed on a chain and plans to deploy to different chains, the attacker can immediately mimic a `SubAccount` on the original chain to deploy a fake one in the new chain tied to the `Console`. Later when the operators start to use `Console` on the new chain, they may think that their `SubAccount` was already deployed on this chain because of the similarities.

## Proof of Concept

1. navigate to `test/branch-trees/SafeDeployer/deploy-console-account/DeploySubAccount.t.sol`
2. import the following safe contracts in the top of the file:
```solidity
import {Enum} from "safe-contracts/common/Enum.sol";
import {GnosisSafe} from "safe-contracts/GnosisSafe.sol";
import {ModuleManager} from "safe-contracts/base/ModuleManager.sol";
```
3.  copy the below proof of concept (with `executeSafeTxHelper()` and `deployBackdoorSubAccountHelper()` helper functions) in `SafeDeployer_DeploySubAccountTest` contract
4. run `make test_func P=testDeploySubAccount_BaitBackdoor_0xfuje`
```solidity
   function testDeploySubAccount_BaitBackdoor_0xfuje() public {
        address attacker = makeAddr("attacker");
        address[] memory owners = new address[](3);
Backdoored module can execute any transactions without consensus and take over `Console` or `SubAccount`
Audit: 2023-10-brahma-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-10-brahma/blob/main/contracts/lib/safe-contracts/contracts/base/ModuleManager.sol#L61-L73
https://github.com/code-423n4/2023-10-brahma/blob/main/contracts/lib/safe-contracts/contracts/base/ModuleManager.sol#L32-L40
https://github.com/code-423n4/2023-10-brahma/blob/main/contracts/src/core/TransactionValidator.sol#L64-L107


# Vulnerability details


## Impact
Complete takeover of [`Console`](https://github.com/code-423n4/2023-10-brahma/blob/main/contracts/src/core/SafeDeployer.sol#L56-L71) or [`SubAccount`](https://github.com/code-423n4/2023-10-brahma/blob/main/contracts/src/core/SafeDeployer.sol#L82-L103) `Safe` with user funds stolen. An attacker could also ask for ransom to "unlock" the `Safe` and reset the original owners and original state.

## Description
A malicious module can be accidentally or intentionally set via [`enableModule()`](https://github.com/code-423n4/2023-10-brahma/blob/main/contracts/lib/safe-contracts/contracts/base/ModuleManager.sol#L32-L40) of `Safe` that can steal all user funds, renounce other owners and take ownership of the `Safe`. If the threshold is set to one at `Safe` initialization a single signer can enable such module. 

Even with a higher threshold, consensus may enable a malicious module, for example it might seem a harmless contract, but with an upgrade-able proxy that can upgrade the implementation to be malicious. Also note that `Safe` doesn't check if the `module` address is a contract, an EOA can be set as a `module`.

### Why restricting modules is important for security
`Safe` modules have unlimited power to execute transactions and can skip any future checks. Allowing arbitrary modules is a big security risk as it can take complete control over the safe. There is no checks performed for `execTransactionFromModule()` calls below 1.5 safe contracts.

`safe-contracts/contracts/base/ModuleManager.sol` - [`execTransactionFromModule()`](https://github.com/code-423n4/2023-10-brahma/blob/main/contracts/lib/safe-contracts/contracts/base/ModuleManager.sol#L61-L73)
```solidity
    function execTransactionFromModule(
        address to,
        uint256 value,
        bytes memory data,
        Enum.Operation operation
    ) public virtual returns (bool success) {
        // Only whitelisted modules are allowed.
        require(msg.sender != SENTINEL_MODULES && modules[msg.sender] != address(0), "GS104");
        // Execute transaction without further confirmations.
        success = execute(to, value, data, operation, type(uint256).max);
        if (success) emit ExecutionFromModuleSuccess(msg.sender);
        else emit ExecutionFromModuleFailure(msg.sender);
    }
```

## Proof of Concept
1. navigate to `test/branch-trees/SafeDeployer/deploy-console-account/DeployConsoleAccount.t.sol`
2. import the following safe contracts in the top of the file:
```solidity
import {OwnerManager} from "safe-contracts/base/OwnerManager.sol";
import {ModuleManager} from "safe-con
Wrong logic in AUTO_COMPOUND doesn't allow for swap to token1
Audit: 2024-06-krystal-defi-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-06-krystal-defi/blob/f65b381b258290653fa638019a5a134c4ef90ba8/src/V3Automation.sol#L158


# Vulnerability details

## Impact
Protocol functionality broken

## Proof of Concept
`AUTO_COMPOUND` action allows for compound your gains into liquidity. It additionally allows for swaps in the middle. There is a faulty condition though, which is never effective and does not allow to set `token1` as targetToken, namely `else if (state.token0 == state.token1)`:

```javascript
        } else if (params.action == Action.AUTO_COMPOUND) {
            if (params.targetToken == state.token0) {
                _swapAndIncrease(
//[...]

            // @audit it should be params.targetToken == state.token1 Currently it doesn't allow for a swap
@>          } else if (state.token0 == state.token1) {
                _swapAndIncrease(
//[...]

            } else {
                // compound without swap
                _swapAndIncrease(
//[...]
            }
```

Because pools cannot have the same token0 and token1, there is no possible position that will fulfill this condition. Looking at other parts of the codebase and params passed to `_swapAndIncrease()` in this code branch, what the protocol wants to achieve is to check if `params.targetToken == state.token1` and perform swap similarly to `params.targetToken == state.token0` branch.

## Tools Used
Manual Review

## Recommended Mitigation Steps

```diff
        } else if (params.action == Action.AUTO_COMPOUND) {
            if (params.targetToken == state.token0) {
                _swapAndIncrease(SwapAndIncreaseLiquidityParams(params.protocol, params.nfpm, params.tokenId, state.amount0, state.amount1, 0, positionOwner, params.deadline, IERC20(state.token1), params.amountIn1, params.amountOut1Min, params.swapData1, 0, 0, bytes(""), params.amountAddMin0, params.amountAddMin1, 0), IERC20(state.token0), IERC20(state.token1), false);
-           } else if (state.token0 == state.token1) {
+           } else if (params.targetToken == state.token1) {
                _swapAndIncrease(SwapAndIncreaseLiquidityParams(params.protocol, params.nfpm, params.tokenId, state.amount0, state.amount1, 0, positionOwner, params.deadline, IERC20(state.token0), 0, 0, bytes(""), params.amountIn0, params.amountOut0Min, params.swapData0, params.amountAddMin0, params.amountAddMin1, 0), IERC20(state.token0), IERC20(state.token1), false);
```


## Assessed type

Error
Tokens values of decimals are not justified in fee deduction
Audit: 2024-06-krystal-defi-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-06-krystal-defi/blob/f65b381b258290653fa638019a5a134c4ef90ba8/src/Common.sol#L657-L695


# Vulnerability details



## Impact
Loss of funds
## Proof of Concept
`SwapAndMint` swaps the given path of tokens or token to the set of tokens:
```solidity
Contract: V3Utils.sol

191:     function swapAndMint(SwapAndMintParams calldata params)  whenNotPaused() external payable returns (SwapAndMintResult memory result) {
192:         if (params.token0 == params.token1) {
193:             revert SameToken();
194:         }
195:         require(_isWhitelistedNfpm(address(params.nfpm)));
196:         IWETH9 weth = _getWeth9(params.nfpm, params.protocol);
197: 
198:         // validate if amount2 is enough for action
199:         if (params.swapSourceToken != params.token0 
200:             && params.swapSourceToken != params.token1
201:             && params.amountIn0 + params.amountIn1 > params.amount2
202:         ) {
203:             revert AmountError();
204:         }
205:         _prepareSwap(weth, params.token0, params.token1, params.swapSourceToken, params.amount0, params.amount1, params.amount2);
206:         SwapAndMintParams memory _params = params;
207: 
208:         DeductFeesEventData memory eventData;
209:         if (params.protocolFeeX64 > 0) {
210:             uint256 feeAmount0;
211:             uint256 feeAmount1;
212:             uint256 feeAmount2;
213:             // since we do not have the tokenId here, we need to emit event later
214:             (_params.amount0, _params.amount1, _params.amount2, feeAmount0, feeAmount1, feeAmount2) = _deductFees(DeductFeesParams(params.amount0, params.amount1, params.amount2, params.protocolFeeX64, FeeType.PROTOCOL_FEE, address(params.nfpm), 0, params.recipient, address(params.token0), address(params.token1), address(params.swapSourceToken)), false);
215: 
216:             eventData = DeductFeesEventData({
217:                 token0: address(params.token0),
218:                 token1: address(params.token1),
219:                 token2: address(params.swapSourceToken),
220:                 amount0: params.amount0,
221:                 amount1: params.amount1,
222:                 amount2: params.amount2,
223:                 feeAmount0: feeAmount0,
224:                 feeAmount1: feeAmount1,
225:                 feeAmount2: feeAmount2,
226:                 feeX64: params.protocolFeeX64,
227:                 feeType: FeeType.PROTOCOL_FEE
228:             });
229:         }
230:         result = _swapAndMint(_params, msg.value != 0);
231:         emit DeductFees(address(params.nfpm), result.tokenId, params.recipient, eventData);
232:     }
```

At L:205 the function calls `_prepareSwap` to convert `msg.value` to the chosen protocol´s WETH and pulls more funds if required.
At L:214, the fees are deducted by passing the params, and `_deductFees` is called.
And finally `_swapAndMint` is called at L: 230.

`_swapAndMint` function is below:
```solidity
Users can bypass fees at all
Audit: 2024-06-krystal-defi-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-06-krystal-defi/blob/f65b381b258290653fa638019a5a134c4ef90ba8/src/V3Utils.sol#L76-L107


# Vulnerability details


## Impact
Loss of protocol fees & funds
## Proof of Concept

The fees are also passed in `execute` function as crafted inside the `Instructions` struct calldata:
```solidity
Contract: V3Utils.sol

 76:     function execute(INonfungiblePositionManager _nfpm, uint256 tokenId, Instructions calldata instructions)  whenNotPaused() external
 77:     {
 78:         // must be approved beforehand
 79:         _nfpm.safeTransferFrom(
 80:             msg.sender,
 81:             address(this),
 82:             tokenId,
 83:             abi.encode(instructions)
 84:         );
 85:     }
 86: 
 87:     /// @notice ERC721 callback function. Called on safeTransferFrom and does manipulation as configured in encoded Instructions parameter. 
 88:     /// At the end the NFT (and any newly minted NFT) is returned to sender. The leftover tokens are sent to instructions.recipient.
 89:     function onERC721Received(address, address from, uint256 tokenId, bytes calldata data)  whenNotPaused() external override returns (bytes4) {
 90:         INonfungiblePositionManager nfpm = INonfungiblePositionManager(msg.sender);
 91:         // not allowed to send to itself
 92:         if (from == address(this)) {
 93:             revert SelfSend();
 94:         }
 95: 
 96:         require(_isWhitelistedNfpm(address(nfpm)));
 97: 
 98:         Instructions memory instructions = abi.decode(data, (Instructions));
 99: 
100:         (address token0,address token1,,,,uint24 fee) = _getPosition(nfpm, instructions.protocol, tokenId);
101: 
102:         (uint256 amount0, uint256 amount1) = _decreaseLiquidityAndCollectFees(DecreaseAndCollectFeesParams(nfpm, instructions.recipient, IERC20(token0), IERC20(token1), tokenId, instructions.liquidity, instructions.deadline, instructions.amountRemoveMin0, instructions.amountRemoveMin1, instructions.compoundFees));
103: 
104:         // take protocol fees
105:  >      if (instructions.protocolFeeX64 > 0) { //@audit Fees can be bypassed by passing 0
106:             (amount0, amount1,,,,) = _deductFees(DeductFeesParams(amount0, amount1, 0, instructions.protocolFeeX64, FeeType.PROTOCOL_FEE, address(nfpm), tokenId, instructions.recipient, token0, token1, address(0)), true);
107:         }
```

So it´s possible to handcraft the fee as `0` in L:76 inside calldata

It will be executed in L: 105 if **only** it´s greater than `0`

This leads to bypassing fees being deducted.
And below snippet remains obsolete:
```solidity
Contract: Common.sol

674:         if (params.feeX64 == 0) {
675:             revert NoFees();
676:         }
```
The same vulnerability is applicable to gas fees too:
```solidity
Contract: V3Automation.sol

105:             if (params.gasFeeX64 > 0) { 
106:                 (,,, gasFeeAmount0, gasFeeAmount1,) = _deductFees(DeductFeesParams(state.amount0, state.amount1,
Having gas fees dependent on amounts is not correct
Audit: 2024-06-krystal-defi-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-06-krystal-defi/blob/f65b381b258290653fa638019a5a134c4ef90ba8/src/V3Automation.sol#L102-L115


# Vulnerability details



## Impact
Loss of funds for user & protocol
## Proof of Concept
The protocol collects Gas Fees for offline orders and currently the fees are calculated based on amounts as below:
```solidity
Contract: V3Automation.sol

102:         {
103:             uint256 gasFeeAmount0;
104:             uint256 gasFeeAmount1;
105:             if (params.gasFeeX64 > 0) {
106:                 (,,, gasFeeAmount0, gasFeeAmount1,) = _deductFees(DeductFeesParams(state.amount0, state.amount1, 0, params.gasFeeX64, FeeType.GAS_FEE, address(params.nfpm), params.tokenId, positionOwner, state.token0, state.token1, address(0)), true);
107:             }
108:             uint256 protocolFeeAmount0;
109:             uint256 protocolFeeAmount1;
110:             if (params.protocolFeeX64 > 0) {
111:                 (,,, protocolFeeAmount0, protocolFeeAmount1,) = _deductFees(DeductFeesParams(state.amount0, state.amount1, 0, params.protocolFeeX64, FeeType.PROTOCOL_FEE, address(params.nfpm), params.tokenId, positionOwner, state.token0, state.token1, address(0)), true);
112:             }
113:             state.amount0 = state.amount0 - gasFeeAmount0 - protocolFeeAmount0;
114:             state.amount1 = state.amount1 - gasFeeAmount1 - protocolFeeAmount1;
115:         }
```
Accordingly, the fees are deducted from the positions and sent to the protocol address in order to use it during `execution` calls.

However, this flow is not correct due to there´s no logic behind gas price calculation.

If the deducted amount is greater than the gas to spend, it´s not returned to the user anywhere in the code.
E.g.;
1. Alice has a position of 10_000 DAI 
2. She wants to have an `AUTO_COMPOUND` action and accordingly set her offline order
3. The gas fee is 1% and it deducts 100 DAI from her position
4. The execution costs 25 USD in Ethereum Mainnet
5. 75 USD is not returned to Alice at the end of the `V3Automation::execute` function

AND

If 1% of the amounts are deducted from the user amounts, it´s not a guaranteed execution no matter if the call to this function succeeds.

In such situation, where the deducted amount is not covering the gas cost, the call will either fail or be spent from the OPERATOR account.



## Tools Used
Manual Review 
## Recommended Mitigation Steps
We recommend the traditional approach where the function´s gas consumption is calculated as:
```solidity
uint256 firstGas = gasLeft();
// .... do all the executions here ..//
uint256 lastGas = gasLeft();
uint256 gasCons = firstGas - lastGas;
```
And finally query the network with `GASPRICE` opcode and see whether the provided funds are sufficient to cover the required gas after calculating with all these params.






## Assessed type

Other