xanoutas/solidity-security-findings huggingface dataset:
The platform fees can be bypassed and the users can migrate to the other platforms
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#L104


# Vulnerability details



## Impact
Loss of future platform fees, business logic is broken
## Proof of Concept
The platform fees can be bypassed as the fee collection structure is actually based on users´ click actions on the platform. It´s not enforced!

Fee collection is based on actions that the users can execute themselves by using the platforms (Uni & Algebra) that they have liquidity. They only need to pay the gas fees in that situation - nothing else. Applying fees for the flows that can be done in cheaper actions will cause the users to flee to the cheaper ones.

As per the contracts, the protocol´s max fee is 10% and  these fees can be totally bypassed by setting 0 fees in `V3Utils`:
```solidity
Contract: Common.sol

104:         _maxFeeX64[FeeType.PROTOCOL_FEE] = 1844674407370955264; // 10%
```

Even if it´s set to 1% later on (or even less), an adversary can anytime copy the Krystal Contracts (with their own settings), build a browser extension tool to interact with the copycat contracts and make the fees less - even 0 fee - and people can again use Krystal´s website to see their positions. In contrast, they interact with the less fee-structured contracts. 

In this situation, the platform will only serve as a complimentary frontend, hence, the business logic will be broken.

## Tools Used
Manual Review 
## Recommended Mitigation Steps
We recommend that the position NFTs should not remain with the users.
 
The positions should belong to users via Krystal´s custom NFTs or with other kinds of validations (e.g. mappings), but the positions should strictly be managed by Krystal DeFi only. 

By doing this, The Krystal DeFi can even perform a liquidity aggregation like the Sushi did in the past.










## Assessed type

Other
Wrong logic in AUTO_COMPOUND doesn't allow for swap to token1 
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#L158


# Vulnerability details



## Impact
Protocol functionality broken

## Proof of Concept
`AUTO_COMPOUND` action allows for compounding 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

Other
The signatures are replayable
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#L79-L81
 https://github.com/code-423n4/2024-06-krystal-defi/blob/f65b381b258290653fa638019a5a134c4ef90ba8/src/StructHash.sol#L274-L296


# Vulnerability details




## Impact
User signed orders can be replayed


## Proof of Concept
All orders managed by the Krystal team are signed by the user, executed by the operator and the signature is verified on-chain:

```solidity
src/V3Automation.sol
    function execute(ExecuteParams calldata params) public payable onlyRole(OPERATOR_ROLE) whenNotPaused() {
        require(_isWhitelistedNfpm(address(params.nfpm)));
        address positionOwner = params.nfpm.ownerOf(params.tokenId);
@>      _validateOrder(params.userOrder, params.orderSignature, positionOwner);
        _execute(params, positionOwner);
    }
//[...]
    function _validateOrder(StructHash.Order memory order, bytes memory orderSignature, address actor) internal view {
@>      address userAddress = recover(order, orderSignature);
@>      require(userAddress == actor);
        require(!_cancelledOrder[keccak256(orderSignature)]);
    }
```

```solidity
src/EIP712.sol
    function recover(StructHash.Order memory order, bytes memory signature) internal view returns (address) {
@>      bytes32 digest = _hashTypedDataV4(StructHash._hash(order));
@>      return ECDSA.recover(digest, signature);
    }
```

However, there is no nonce field in the [StructHash.Order](https://github.com/code-423n4/2024-06-krystal-defi/blob/f65b381b258290653fa638019a5a134c4ef90ba8/src/StructHash.sol). That means that the same hashed order can be reused multiple times. What's also important is that the order is used only to verify if it was signed by the user, however it's not checked if the order that user signed is the same as the params passed by the operator, because [ExecuteParams](https://github.com/code-423n4/2024-06-krystal-defi/blob/f65b381b258290653fa638019a5a134c4ef90ba8/src/V3Automation.sol#L40-L82) sent to `V3Automation.execute()` use other set of params to indicate the action to perform:

```solidity
    struct ExecuteParams {
        Action action;
        Protocol protocol;
        INonfungiblePositionManager nfpm;

        uint256 tokenId;
        uint128 liquidity; // liquidity the calculations are based on

        // target token for swaps (if this is address(0) no swaps are executed)
        address targetToken;
    
        uint256 amountIn0;
        // if token0 needs to be swapped to targetToken - set values
        uint256 amountOut0Min;
        bytes swapData0;
        //[...]
        // user signed config
        StructHash.Order userOrder;
        bytes orderSignature;
    }
```

And later in the function, `params.userOrder` is not used, only the params prepared by the operator:

```solidity
src/V3Automation.sol
@>      if (params.action == Action.AUTO_ADJUST) {
            require(state.tickLower != params.newTi
`_swapAndMint()` does not use `safeTransferFrom`
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#L392


# Vulnerability details



## Impact
Unsafe transfer of ERC721 tokens in `_swapAndMint()`.

## Proof of Concept
[`_swapAndMint()` claims to be using `safeTransferFrom`](https://github.com/code-423n4/2024-06-krystal-defi/blob/f65b381b258290653fa638019a5a134c4ef90ba8/src/Common.sol#L360), but [then only uses `transferFrom`](https://github.com/code-423n4/2024-06-krystal-defi/blob/f65b381b258290653fa638019a5a134c4ef90ba8/src/Common.sol#L392).
This is an issue especially in `V3Utils` where the user might rely on compliance with this safety check on the `instructions.recipient` as [`_swapAndMint()` is called during the `V3Utils.execute()` in `V3Utils.onERC721Received()`](https://github.com/code-423n4/2024-06-krystal-defi/blob/f65b381b258290653fa638019a5a134c4ef90ba8/src/V3Utils.sol#L129-L134)

## Recommended Mitigation Steps
Use `safeTransferFrom()` on Common.sol#L392.


## Assessed type

ERC721
`_deductFees() is incompatible with tokens that revert on zero value transfers
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#L668-L682


# Vulnerability details


## Impact
All main functionality risks reverting with tokens that revert on zero value transfers, via a transfer in `Common._deductFees()`.

## Proof of Concept
In `Common._deductFees()`
```solidity
if (params.feeX64 == 0) {
    revert NoFees();
}

if (params.amount0 > 0) {
    feeAmount0 = FullMath.mulDiv(params.amount0, params.feeX64, Q64);
    amount0Left = params.amount0 - feeAmount0;
    SafeERC20.safeTransfer(IERC20(params.token0), FEE_TAKER, feeAmount0);
}
if (params.amount1 > 0) {
    feeAmount1 = FullMath.mulDiv(params.amount1, params.feeX64, Q64);
    amount1Left = params.amount1 - feeAmount1;
    SafeERC20.safeTransfer(IERC20(params.token1), FEE_TAKER, feeAmount1);
}
if (params.amount2 > 0) {
    feeAmount2 = FullMath.mulDiv(params.amount2, params.feeX64, Q64);
    amount2Left = params.amount2 - feeAmount2;
    SafeERC20.safeTransfer(IERC20(params.token2), FEE_TAKER, feeAmount2);
}
```
if `0 < params.feeX64 * params.amountX < Q64` then `feeAmountX = 0`. In this case the `SafeERC20.safeTransfer()` reverts on a token which reverts on zero transfers.

`_deductFees()` is used throughout the functionality of `V3Automation` and `V3Utils`.

## Recommended Mitigation Steps
```solidity
if (feeAmount0 > 0) {
    SafeERC20.safeTransfer(IERC20(params.token0), FEE_TAKER, feeAmount0);
}
```
etc.



## Assessed type

ERC20
Inconsistent and potentially reverting handling of tokens that do not allow an approval amount of 0
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#L551


# Vulnerability details



## Impact
`Common._swap()` may revert for tokens that do not allow an approval amount of 0.

## Proof of Concept
As [indicated by `Common._safeResetAndApprove()`](https://github.com/code-423n4/2024-06-krystal-defi/blob/f65b381b258290653fa638019a5a134c4ef90ba8/src/Common.sol#L714) some tokens do not allow an approval amount of 0:
```solidity
/// @dev some tokens require allowance == 0 to approve new amount
/// but some tokens does not allow approve ammount = 0
/// we try to set allowance = 0 before approve new amount. if it revert means that
/// the token not allow to approve 0, which means the following line code will work properly
function _safeResetAndApprove(IERC20 token, address _spender, uint256 _value) internal {
    /// @dev ommited approve(0) result because it might fail and does not break the flow
    address(token).call(abi.encodeWithSelector(token.approve.selector, _spender, 0));

    /// @dev value for approval after reset must greater than 0
    require(_value > 0);
    _safeApprove(token, _spender, _value);
}
```
However, inside `Common._swap()` such an approval of 0 is attempted at L551 [`_safeApprove(tokenIn, swapRouter, 0);`](https://github.com/code-423n4/2024-06-krystal-defi/blob/f65b381b258290653fa638019a5a134c4ef90ba8/src/Common.sol#L551), where [`_safeApprove()`](https://github.com/code-423n4/2024-06-krystal-defi/blob/f65b381b258290653fa638019a5a134c4ef90ba8/src/Common.sol#L726-L731) is:
```solidity
function _safeApprove(IERC20 token, address _spender, uint256 _value) internal {
    bytes memory returnData = address(token).functionCall(abi.encodeWithSelector(token.approve.selector, _spender, _value));
    if (returnData.length > 0) { // Return data is optional
        require(abi.decode(returnData, (bool)), "SafeERC20: ERC20 operation did not succeed");
    }
}
```
This means that L551 may cause a revert with such a token.

## Recommended Mitigation Steps
Take precautions similar to those in `_safeResetAndApprove()`, setting approval to `1` in case of failure.


## Assessed type

ERC20
User sets the fees himself
Audit: 2024-06-krystal-defi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-06-krystal-defi/blob/f65b381b258290653fa638019a5a134c4ef90ba8/src/V3Utils.sol#L105-L106
https://github.com/code-423n4/2024-06-krystal-defi/blob/f65b381b258290653fa638019a5a134c4ef90ba8/src/V3Utils.sol#L209-L214
https://github.com/code-423n4/2024-06-krystal-defi/blob/f65b381b258290653fa638019a5a134c4ef90ba8/src/V3Utils.sol#L254-L255


# Vulnerability details



## Impact
User can avoid paying protocol fees in `V3Utils`.

## Proof of Concept
In `V3Utils` the protocol fees are deducted by `Common._deductFees()` according to a user supplied parameter `protocolFeeX64`, [in `onERC721Received()`](https://github.com/code-423n4/2024-06-krystal-defi/blob/f65b381b258290653fa638019a5a134c4ef90ba8/src/V3Utils.sol#L105-L106), [´in swapAndMint()´](https://github.com/code-423n4/2024-06-krystal-defi/blob/f65b381b258290653fa638019a5a134c4ef90ba8/src/V3Utils.sol#L209-L214) and [´in swapAndIncreaseLiquidity()´](https://github.com/code-423n4/2024-06-krystal-defi/blob/f65b381b258290653fa638019a5a134c4ef90ba8/src/V3Utils.sol#L254-L255). The only restriction placed on this is that it [not surpass the `_maxFeeX64`](https://github.com/code-423n4/2024-06-krystal-defi/blob/f65b381b258290653fa638019a5a134c4ef90ba8/src/Common.sol#L659-L661). The user is thus free to set it to 0, avoiding all fees.

## Recommended Mitigation Steps
Have a fixed fee in `Common._deductFees()` set by the admin.


## Assessed type

Other
Replayable order signature
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#L79-L81
https://github.com/code-423n4/2024-06-krystal-defi/blob/f65b381b258290653fa638019a5a134c4ef90ba8/src/StructHash.sol#L274-L296


# Vulnerability details

## Impact
User signed orders can be replayed


## Proof of Concept
All orders managed by Krystal team are signed by the user, executed by operator and the signature is verified on-chain:

```javascript
src/V3Automation.sol
    function execute(ExecuteParams calldata params) public payable onlyRole(OPERATOR_ROLE) whenNotPaused() {
        require(_isWhitelistedNfpm(address(params.nfpm)));
        address positionOwner = params.nfpm.ownerOf(params.tokenId);
@>      _validateOrder(params.userOrder, params.orderSignature, positionOwner);
        _execute(params, positionOwner);
    }
//[...]
    function _validateOrder(StructHash.Order memory order, bytes memory orderSignature, address actor) internal view {
@>      address userAddress = recover(order, orderSignature);
@>      require(userAddress == actor);
        require(!_cancelledOrder[keccak256(orderSignature)]);
    }
```

```javascript
src/EIP712.sol
    function recover(StructHash.Order memory order, bytes memory signature) internal view returns (address) {
@>      bytes32 digest = _hashTypedDataV4(StructHash._hash(order));
@>      return ECDSA.recover(digest, signature);
    }
```

However, there is no nonce field in the [StructHash.Order](https://github.com/code-423n4/2024-06-krystal-defi/blob/f65b381b258290653fa638019a5a134c4ef90ba8/src/StructHash.sol). That means that the same hashed order can be reused multiple times. What's also important is that the order is used only to verify if it was signed by the user, however it's not checked if the order that user signed is the same as the params passed by the operator, because [ExecuteParams](https://github.com/code-423n4/2024-06-krystal-defi/blob/f65b381b258290653fa638019a5a134c4ef90ba8/src/V3Automation.sol#L40-L82) sent to `V3Automation.execute()` use other set of params to indicate the action to perform:

```javascript
    struct ExecuteParams {
        Action action;
        Protocol protocol;
        INonfungiblePositionManager nfpm;

        uint256 tokenId;
        uint128 liquidity; // liquidity the calculations are based on

        // target token for swaps (if this is address(0) no swaps are executed)
        address targetToken;
    
        uint256 amountIn0;
        // if token0 needs to be swapped to targetToken - set values
        uint256 amountOut0Min;
        bytes swapData0;
        //[...]
        // user signed config
        StructHash.Order userOrder;
        bytes orderSignature;
    }
```

And later in the function, `params.userOrder` is not used, only the params prepared by the operator:

```javascript
src/V3Automation.sol
@>      if (params.action == Action.AUTO_ADJUST) {
            require(state.tickLower != params.newTickLo
The Protocol breaks the Allowance Mechanism of the NFTs
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#L392
https://github.com/code-423n4/2024-06-krystal-defi/blob/f65b381b258290653fa638019a5a134c4ef90ba8/src/V3Utils.sol#L76-L85
https://github.com/code-423n4/2024-06-krystal-defi/blob/f65b381b258290653fa638019a5a134c4ef90ba8/src/V3Utils.sol#L171
https://github.com/code-423n4/2024-06-krystal-defi/blob/f65b381b258290653fa638019a5a134c4ef90ba8/src/V3Automation.sol#L92


# Vulnerability details



## Impact
The user loses their approved entities
## Proof of Concept
The protocol transfers the Position NFT from the position owner to the protocol and back to the position owner to execute the actions.
<details>
<summary>List of occurences</summary>

```solidity
Contract: Common.sol

402:         params.nfpm.transferFrom(address(this), params.recipient, result.tokenId);
```
```solidity
Contract: V3Utils.sol

171:         nfpm.transferFrom(address(this), from, tokenId);
```
```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:     }
```
```solidity
Contract: V3Automation.sol

92:         params.nfpm.transferFrom(positionOwner, address(this), params.tokenId);
```
```solidity
Contract: V3Automation.sol

167:         params.nfpm.transferFrom(address(this), positionOwner, params.tokenId);
```
</details>

This breaks the allowance mechanism of ERC721 for every action taken on the platform and it omits the allowances given from the token owners to the approved entities.
For every NFT transfer, the allowances are zeroed out:
```solidity
Contract: ERC721.sol

333:     function _transfer(address from, address to, uint256 tokenId) internal virtual {
334:         require(ERC721.ownerOf(tokenId) == from, "ERC721: transfer from incorrect owner");
335:         require(to != address(0), "ERC721: transfer to the zero address");
336: 
337:         _beforeTokenTransfer(from, to, tokenId, 1);
338: 
339:         // Check that tokenId was not transferred by `_beforeTokenTransfer` hook
340:         require(ERC721.ownerOf(tokenId) == from, "ERC721: transfer from incorrect owner");
341: 
342:         // Clear approvals from the previous owner
343:   >>    delete _tokenApprovals[tokenId];
344: 
345:         unchecked {
346:             // `_balances[from]` cannot overflow for the same reason as described in `_burn`:
347:             // `from`'s balance is the number of token held, which is at least one before the current
348:             // transfer.
349:             // `_balances[to]` could overflow in the conditions described in `_mint`. That would require
350:             // all 2**256 token ids to be minte
The Protocol doesn´t cover all the Liquidity Positions as declared
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#L640


# Vulnerability details



## Impact
Inability and duality in LP management, breaking the business logic
## Proof of Concept
The website [states](https://docs.krystal.app/#why-krystal) that:

> While most liquidity management platforms have their own preset strategies that LPs cannot modify, Krystal believes in empowering LPs to customize their own strategies. We aim to be the best liquidity management platform by providing advanced tools and analysis for **both passive and active LPs** to build customized strategies that suit their needs.

So the declaration is to manage all the liquidity positions.

However, if a user has a position created by `pool.mint` and later on with a call from `nfpm.mint` either through Krystal or not, the Krystal only sees and parses the `nfpm` positions by calling `position` method.

This is because the positions created by `pool.mint` and the positions created by `NonfungiblePositionManager.mint` are tracked separately. The former is an internal record in the pool contract, while the latter is an NFT.


So the protocol is not fulfilling their declaration in their implementation.

E.g.:
1. Alice has a position of 10_000 DAI where she created her LP with `pool.mint` 
2. Alice mints another position by calling `nfpm.mint` and holds 15_000 USDC for a different LP
3. Alice logs into Krystal Dapp and she only sees her DAI LP. 
4. Alice doesn´t use the Dapp again since it´s not fulfilling the declared feature - LP management center

## Tools Used
Manual Review 
## Recommended Mitigation Steps
Implement the getter function of Uniswap´s `Position.sol` library too.





## Assessed type

Other