xanoutas/solidity-security-findings huggingface dataset:
Not having granulated fees leads to loss of funds 
Audit: 2024-06-krystal-defi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details



## Impact
Users will lose their assets to the fees for the action that is not worth for it.
## Proof of Concept
The Krystal DeFi applies only `one` type of fee for the protocol known as the `PROTOCOL_FEE`.

Currently, it´s set to max 10% as per the contracts while it can be changed anytime as the contracts have a setter for that.

```solidity
Contract: Common.sol

103:         _maxFeeX64[FeeType.GAS_FEE] = 1844674407370955264; // 10% 
104:         _maxFeeX64[FeeType.PROTOCOL_FEE] = 1844674407370955264; // 10%
```
The protocol has one protocol fee  - not as granulated as `mintfee`, `swap fee`, `change liquidity fee`, `collect fee` etc.
The lack of projected fees leads to the users losing their funds to the protocol fees from their positions.

Below you can see the logic of deducting fees.
```solidity
Contract: Common.sol

662:     /**
663:      * @notice calculate fee
664:      * @param emitEvent: whether to emit event or not. Since swap and mint have not had token id yet.
665:      * we need to emit event latter
666:      */ 
667:     function _deductFees(DeductFeesParams memory params, bool emitEvent) internal returns(uint256 amount0Left, uint256 amount1Left, uint256 amount2Left, uint256 feeAmount0, uint256 feeAmount1, uint256 feeAmount2) {
668:         uint256 Q64 = 2 ** 64;// 18 eth
669:         if (params.feeX64 > _maxFeeX64[params.feeType]) { 
670:             revert TooMuchFee();
671:         }
672: 
673:         // to save gas, we always need to check if fee exists before deductFees
674:         if (params.feeX64 == 0) {
675:             revert NoFees();
676:         }
677: 
678:         if (params.amount0 > 0) {
679:             feeAmount0 = FullMath.mulDiv(params.amount0, params.feeX64, Q64); 
680:             amount0Left = params.amount0 - feeAmount0;
681:             SafeERC20.safeTransfer(IERC20(params.token0), FEE_TAKER, feeAmount0);
682:         }
683:         if (params.amount1 > 0) {
684:             feeAmount1 = FullMath.mulDiv(params.amount1, params.feeX64, Q64);
685:             amount1Left = params.amount1 - feeAmount1;
686:             SafeERC20.safeTransfer(IERC20(params.token1), FEE_TAKER, feeAmount1);
687:         }
688:         if (params.amount2 > 0) {
689:             feeAmount2 = FullMath.mulDiv(params.amount2, params.feeX64, Q64);
690:             amount2Left = params.amount2 - feeAmount2;
691:             SafeERC20.safeTransfer(IERC20(params.token2), FEE_TAKER, feeAmount2);
692:         }
693: 
694: 
695:         if (emitEvent) {
696:             emit DeductFees(address(params.nfpm), params.tokenId, params.userAddress, DeductFeesEventData(
697:                 params.token0, params.token1, params.token2, 
698:                 params.amount0, params.amount1, params.amount2, 
699:                 feeAmount0, feeAmount1, feeAmount2,
700:                 p
Non-compliance with EIP712
Audit: 2024-06-krystal-defi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Invalid signatures 
## Proof of Concept
`StructHash` contract prepares the user signatures in the structure of Krystal functions.
One of them is hashing `RebalanceConfig` struct.

```solidity
Contract: StructHash.sol

190:     struct RebalanceConfig {
191:         RebalanceCondition rebalanceCondition;
192:         RebalanceAction rebalanceAction;
193:         AutoCompound autoCompound;
194:         bool recurring;
195:     }
196:     function _hash(RebalanceConfig memory obj) private pure returns (bytes32) {
197:         return keccak256(abi.encode(
198:             RebalanceConfig_TYPEHASH,
199:             _hash(obj.rebalanceCondition),
200:             _hash(obj.rebalanceAction),
201:             _hash(obj.autoCompound),
202:  >           obj.recurring 
203:         ));
204:     }
```

For Line: 202, since the param is a `boolean`,  to ensure compliance with [EIP-712](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md#definition-of-encodedata), the `bool` should be explicitly casted to a `uint256` when encoding it. 

Below is quoted from the EIP page: 
> The atomic values are encoded as follows: Boolean false and true are encoded as uint256 values 0 and 1 respectively.


## Tools Used
Manual Review 
## Recommended Mitigation Steps
```diff
196:     function _hash(RebalanceConfig memory obj) private pure returns (bytes32) {
197:         return keccak256(abi.encode(
198:             RebalanceConfig_TYPEHASH,
199:             _hash(obj.rebalanceCondition),
200:             _hash(obj.rebalanceAction),
201:             _hash(obj.autoCompound),
-                obj.recurring 
+                obj.recurring ? uint256(1) : uint256(0)
203:         ));
204:     }
```





## Assessed type

Other
Swapping logic would be broken for some supported tokens
Audit: 2024-06-krystal-defi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details


## Proof of Concept

First take a look at this excerpt from the readMe https://github.com/code-423n4/2024-06-krystal-defi/blob/f65b381b258290653fa638019a5a134c4ef90ba8/README.md#L103-L106

```markdown
| Question                                                                                                                                                   | Answer                                      |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
| Chains the protocol will be deployed on                                                                                                                    | Ethereum,Arbitrum,Base,BSC,Optimism,Polygon |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ------                                      |
| [Revert on zero value approvals](https://github.com/d-xo/weird-erc20?tab=readme-ov-file#revert-on-zero-value-approvals)                                    | Yes                                         |
```

From the above we can conclude that tokens like BNB that revert on zero valu approvals are to be integrated within protocol.

Now take a look at https://github.com/code-423n4/2024-06-krystal-defi/blob/f65b381b258290653fa638019a5a134c4ef90ba8/src/Common.sol#L537-L567

```solidity
    function _swap(IERC20 tokenIn, IERC20 tokenOut, uint256 amountIn, uint256 amountOutMin, bytes memory swapData) internal returns (uint256 amountInDelta, uint256 amountOutDelta) {
        if (amountIn != 0 && swapData.length != 0 && address(tokenOut) != address(0)) {
            uint256 balanceInBefore = tokenIn.balanceOf(address(this));
            uint256 balanceOutBefore = tokenOut.balanceOf(address(this));

            // approve needed amount
            _safeApprove(tokenIn, swapRouter, amountIn);
            // execute swap
            (bool success,) = swapRouter.call(swapData);
            if (!success) {
                revert ("swap failed!");
            }

            // reset approval
            //@audit resetting the approval would never work for these tokens
            _safeApprove(tokenIn, swapRouter, 0);

            uint256 balanceInAfter = tokenIn.balanceOf(address(this));
            uint256 balanceOutAfter = tokenOut.balanceOf(address(this));

            amountInDelta = balanceInBefore - balanceInAfter;
            amountOutDelta = balanceOutAfter - balanceOutBefore;

            // amountMin slippage check
            if (amountOutDelta < amountOutMin) {
                revert SlippageError();
            }

            // event for any swap with exact swapped value
Protocol fee is hardcoded to `0` during auto-adjusts
Audit: 2024-06-krystal-defi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details


## Proof of Concept

Take a look at https://github.com/code-423n4/2024-06-krystal-defi/blob/f65b381b258290653fa638019a5a134c4ef90ba8/src/V3Automation.sol#L117-L129

```solidity
    function _execute(ExecuteParams calldata params, address positionOwner) internal {
..
            if (params.protocolFeeX64 > 0) {
                (,,, 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);
            }
            state.amount0 = state.amount0 - gasFeeAmount0 - protocolFeeAmount0;
            state.amount1 = state.amount1 - gasFeeAmount1 - protocolFeeAmount1;
        }
        if (params.action == Action.AUTO_ADJUST) {
            require(state.tickLower != params.newTickLower || state.tickUpper != params.newTickUpper);
            SwapAndMintResult memory result;
            if (params.targetToken == state.token0) {
                result = _swapAndMint(SwapAndMintParams(params.protocol, params.nfpm, IERC20(state.token0), IERC20(state.token1), state.fee, params.newTickLower, params.newTickUpper, 0, state.amount0, state.amount1, 0, positionOwner, params.deadline, IERC20(state.token1), params.amountIn1, params.amountOut1Min, params.swapData1, 0, 0, bytes(""), params.amountAddMin0, params.amountAddMin1), false);
            } else if (params.targetToken == state.token1) {
                result = _swapAndMint(SwapAndMintParams(params.protocol, params.nfpm, IERC20(state.token0), IERC20(state.token1), state.fee, params.newTickLower, params.newTickUpper, 0, state.amount0, state.amount1, 0, positionOwner, params.deadline, IERC20(state.token0), 0, 0, bytes(""), params.amountIn0, params.amountOut0Min, params.swapData0, params.amountAddMin0, params.amountAddMin1), false);
            } else {
                // Rebalance without swap @audit all three _swapAndMint query under AUTO_ADJUST have the protocol fee set as `0`
                result = _swapAndMint(SwapAndMintParams(params.protocol, params.nfpm, IERC20(state.token0), IERC20(state.token1), state.fee, params.newTickLower, params.newTickUpper, 0, state.amount0, state.amount1, 0, positionOwner, params.deadline, IERC20(address(0)), 0, 0, bytes(""), 0, 0, bytes(""), params.amountAddMin0, params.amountAddMin1), false);
            }
            emit ChangeRange(address(params.nfpm), params.tokenId, result.tokenId, result.liquidity, result.added0, result.added1);
..
       }
```

This is the internal execute function that gets always gets queried in the `V3Automation`, and in the case where the action is `AUTO_ADJUST` the `_swapAndMint()` gets called, issue however is that all three instances hardcode the protocol fee as `0`, regardless of what ha
The BSC chain doesn't have a WETH wrapper which would cause issues for protocol's deployment on the chain
Audit: 2024-06-krystal-defi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details


## Proof of Concept

First take a look at this excerpt from the readMe https://github.com/code-423n4/2024-06-krystal-defi/blob/f65b381b258290653fa638019a5a134c4ef90ba8/README.md#L101

```markdown
| Question                                | Answer                                      |
| --------------------------------------- | ------------------------------------------- |
| Chains the protocol will be deployed on | Ethereum,Arbitrum,Base,BSC,Optimism,Polygon |
```

We can see that a supported chain where Protocol plans on deploying to is the Binance smart chain.

Now, in multiple instances across contracts in scope we can see that there is an attempt to withdraw directly from the WETH implementation, i.e see how tokens are being transferred, by first unwrapping and then sending the `ETH` https://github.com/code-423n4/2024-06-krystal-defi/blob/f65b381b258290653fa638019a5a134c4ef90ba8/src/Common.sol#L522-L533

```solidity
    function _transferToken(IWETH9 weth, address to, IERC20 token, uint256 amount, bool unwrap) internal {
        if (address(weth) == address(token) && unwrap) {
            //@audit
            weth.withdraw(amount);
            (bool sent, ) = to.call{value: amount}("");
            if (!sent) {
                revert EtherSendFailed();
            }
        } else {
            SafeERC20.safeTransfer(token, to, amount);
        }
    }

```

Other instances across scope where this function gets used can be seen below

```rust
ib/v3-periphery/contracts/V3Migrator.sol:
  74              if (params.refundAsETH && params.token0 == WETH9) {
  75:                 IWETH9(WETH9).withdraw(refund0);
  76                  TransferHelper.safeTransferETH(msg.sender, refund0);

  87              if (params.refundAsETH && params.token1 == WETH9) {
  88:                 IWETH9(WETH9).withdraw(refund1);
  89                  TransferHelper.safeTransferETH(msg.sender, refund1);

lib/v3-periphery/contracts/base/PeripheryPayments.sol:
  23          if (balanceWETH9 > 0) {
  24:             IWETH9(WETH9).withdraw(balanceWETH9);
  25              TransferHelper.safeTransferETH(recipient, balanceWETH9);

lib/v3-periphery/contracts/base/PeripheryPaymentsWithFee.sol:
  25          if (balanceWETH9 > 0) {
  26:             IWETH9(WETH9).withdraw(balanceWETH9);
  27              uint256 feeAmount = (balanceWETH9 * feeBips) / 10_000;

```

Where as this implementation works on most of the chains where the protocol is to deploy to, the query to `.withdraw()` would always fail on the Binance smart chain. This is because no WETH wrapper exists on BSC, also going to this helper site to get the addresses for WETH: https://www.coingecko.com/en/coins/weth we can see the different addresses for different chains, i.e

- `0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2` on the Mainnet &
- `0x2170ed0880a
Immutable `DOMAIN_SEPARATOR` will become incorrect if any chain protocol deploys on hard forks
Audit: 2024-06-krystal-defi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-06-krystal-defi/blob/f65b381b258290653fa638019a5a134c4ef90ba8/src/EIP712.sol#L11-L23


# Vulnerability details


## Proof of Concept

Take a look at https://github.com/code-423n4/2024-06-krystal-defi/blob/f65b381b258290653fa638019a5a134c4ef90ba8/src/EIP712.sol#L11-L23

```solidity
    bytes32 public immutable DOMAIN_SEPARATOR;

    constructor(string memory name, string memory version) {
        DOMAIN_SEPARATOR = keccak256(
            abi.encode(
                TYPE_HASH,
                keccak256(bytes(name)),
                keccak256(bytes(version)),
                block.chainid,
                address(this)
            )
        );
    }
```

We can see that this is the implementation of how the `DOMAIN_SEPARATOR` is being stored/cached, i.e it's set as an immutable variable and the chain.Id gets queried only once, issue here however is that the protocol doesn't take into account that the chain to which the protocol would be deployed could undergo a hardfork, which would then make the block.chainId attached to `DOMAIN_SEPARATOR` to now be stale.

Note that from the readMe the below has been stated https://github.com/code-423n4/2024-06-krystal-defi/blob/f65b381b258290653fa638019a5a134c4ef90ba8/README.md#L101

```markdown
| Chains the protocol will be deployed on | Ethereum,Arbitrum,Base,BSC,Optimism,Polygon |
```

Which heavily increases the likelihood of this occurring since we are not looking at a hard fork happening on the Ethereum chain only, but rather all supported chains.

Now going to the implemented EIP 712 definition of the `DOMAIN_SEPARATOR` , we can see that this has been hinted on the block.chainId https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator:

> `uint256 chainId` the [EIP-155](https://eips.ethereum.org/EIPS/eip-155) chain id. The user-agent should refuse signing if it does not match the currently active chain.

Note that [EIP-155](https://eips.ethereum.org/EIPS/eip-155) has been out for a while and created around ~8 years ago, and it preaches that the chainIds used should allow replay attack protections.

Which then leads to the core issue on Krystal Defi's implementation being that if any of the chain which protocol is to be deployed on ever hard forks, signature replay becomes possible across the original and forked chain. This is because the immutable `DOMAIN_SEPARATOR` (which contains block.chainid) is pre-computed and will remain the same on the forked chain, even though its chain ID is different.

## Impact

As hinted under _Proof of Concept_, if a chain hard forks, signature replay becomes possible across the original and forked chain.

## Recommended Mitigation Steps

Consider checking if block.chainid matches the chainId cached in the `DOMAIN_SEPARATOR` before using it.

A pseudo fix, could be to introduce like a helper function to calculate the domain separator and this would then check if the `DOMAIN_SEPERATOR` has changed, i.e something l
Execution would fail when tokens like weth is being transferred out in V3Automation on Arbitrum
Audit: 2024-06-krystal-defi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details



## Proof of Concept

Take a look at https://github.com/code-423n4/2024-06-krystal-defi/blob/f65b381b258290653fa638019a5a134c4ef90ba8/src/V3Automation.sol#L91-L168

```solidity
    function _execute(ExecuteParams calldata params, address positionOwner) internal {
..snip
        params.nfpm.transferFrom(address(this), positionOwner, params.tokenId);
    }
```

Evidently at the final step of the execution this is being done.

NB: A similar logic could be applicable [here](https://github.com/code-423n4/2024-06-krystal-defi/blob/f65b381b258290653fa638019a5a134c4ef90ba8/src/V3Utils.sol#L169-L173).

Now note that from the readMe the below has been stated https://github.com/code-423n4/2024-06-krystal-defi/blob/f65b381b258290653fa638019a5a134c4ef90ba8/README.md#L101

```markdown
| Question                                | Answer                                      |
| --------------------------------------- | ------------------------------------------- |
| ERC20 used by the protocol              |       Any             |
| Chains the protocol will be deployed on | Ethereum,Arbitrum,Base,BSC,Optimism,Polygon |
```

Now whereas this would work normally on other chains if `WETH` or tokens like it is integrated and is being transferrred out, it would fail on the Arbitrum chain, which protocol plans to deploy on, this is because the transfer is `transferFrom` and then passing `addressThis` as the `from`, as hinted earlier on this would work fine on most chains (Ethereum, Optimism, Polygon, BSC) which uses the standard `WETH9` contract that handles the case when src == msg.sender, i.e:

```rust

WETH9.sol
        if (src != msg.sender && allowance[src][msg.sender] != uint(- 1)) {
            require(allowance[src][msg.sender] >= wad);
            allowance[src][msg.sender] -= wad;
        }
```

The problem is that the [WETH implementation on Arbitrum](https://arbiscan.io/address/0x8b194beae1d3e0788a1a35173978001acdfba668#code) uses a different contract, and does not have this `src == msg.sender` handling, which makes the transfer attempt to always fail.

Also, the issue is present both on [Blast](https://blastscan.io/address/0x4300000000000000000000000000000000000004#code) and [Wrapped Fantom](https://ftmscan.com/token/0x21be370d5312f44cb42ce377bc9b8a0cef1a4c83#code). ..._just incase protocol decides on deploying there in the future_.

## Impact

Because the contract never approves itself to spend WETH, the [token transfer line](https://github.com/code-423n4/2024-06-krystal-defi/blob/f65b381b258290653fa638019a5a134c4ef90ba8/src/V3Automation.sol#L167) will always revert on Arbitrum when dealing with `WETH`

Categorizing this as Medium given that it impacts the availability of `V3Automation#execute()` on the supported Arbitrum chain with WETH.

## Tool used

- Manual review
- Past findings:
Users can gas grief the operator in the `V3Automation.sol`
Audit: 2024-06-krystal-defi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
The operator may spend gas on the execute call without receiving any value in return.

## Proof of Concept
The Krystal protocol allows users to automate actions such as exiting or reranging positions. When certain conditions are met, the Krystal-controlled wallet (bot) calls the execute function with the user's instructions, and a portion of the tokens is paid as fees to the protocol.

There are several issues with this approach:

- The tokens deducted as fees for gas usage may have no value. For instance, a user could deploy two basic ERC20 contracts (TOK1, TOK2), create a Uniswap pool with them, mint an LP token, and request the Krystal automator to rebalance it. Upon completing the execute transaction, the protocol receives TOK1 and TOK2 as fees, but these tokens may have no market value and cannot be exchanged for ETH or USDC. Consequently, the protocol spends gas without any profit.
- A user could create a malicious token that performs costly computations within the `approve` or `transfer` calls. This would waste the operator's gas when attempting to execute actions with such tokens and eventually revert.
 
## Tools Used
Manual review

## Recommended Mitigation Steps
Implement a whitelist for tokens and verify if LP NFT token0 and token1 is in it.


## Assessed type

Token-Transfer