xanoutas/solidity-security-findings huggingface dataset:
Incorrect fee distribution in `claim()`
Audit: 2024-06-vultisig-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-06-vultisig/blob/main/src/ILOPool.sol#L184
https://github.com/Uniswap/v3-core/blob/main/contracts/UniswapV3Pool.sol#L517
https://github.com/Uniswap/v3-core/blob/main/contracts/UniswapV3Pool.sol#L490


# Vulnerability details

## Impact
Incorrect fee distribution in `claim()`.

## Proof of Concept
After a successful project launch, investors can claim their share of liquidity. The `claim()` function calculates the `liquidity2Claim` to burn. It is important to note that `pool.burn()` recalculates fees owed to a position, crediting `amount0`, `amount1`, and fees accumulated to `position.tokensOwed0` and `position.tokensOwed1`.

```solidity
    /// @notice Burn liquidity from the sender and account tokens owed for the liquidity to the position
    /// @dev Can be used to trigger a recalculation of fees owed to a position by calling with an amount of 0
    /// @dev Fees must be collected separately via a call to #collect
    /// @param tickLower The lower tick of the position for which to burn liquidity
    /// @param tickUpper The upper tick of the position for which to burn liquidity
    /// @param amount How much liquidity to burn
    /// @return amount0 The amount of token0 sent to the recipient
    /// @return amount1 The amount of token1 sent to the recipient
    function burn(
        int24 tickLower,
        int24 tickUpper,
        uint128 amount
    ) external returns (uint256 amount0, uint256 amount1);
```
Upon calling `burn()`, the following updates occur:
```solidity
        if (amount0 > 0 || amount1 > 0) {
            (position.tokensOwed0, position.tokensOwed1) = (
                position.tokensOwed0 + uint128(amount0),
                position.tokensOwed1 + uint128(amount1)
            );
        }
```
In Uniswap V3, `feeGrowthInside0LastX128` and `feeGrowthInside1LastX128` represent the cumulative fees earned per unit of liquidity in `token0` and `token1` within a specific position's range since the last update. When a user claims, they receive their share of `fee0` and `fee1` by multiplying these values by `positionLiquidity`.

The `amount0` and `amount1` variables are incremented by `fees0` and `fees1`, representing the investor's fair share:

```solidity
            amount0 += fees0;
            amount1 += fees1;
```

The function then calls `pool.collect()` to collect all of `token0` and `token1` in `Position.Info`, which includes amounts from `burn()` plus all fees accumulated since the last update:

```solidity
    function collect(
        address recipient,
        int24 tickLower,
        int24 tickUpper,
        uint128 amount0Requested,
        uint128 amount1Requested
    ) external override lock returns (uint128 amount0, uint128 amount1) {
        // we don't need to checkTicks here, because invalid positions will never have non-zero tokensOwed{0,1}
        Position.Info storage position = positions.get(msg.sender, tickLower, tickUpper);

      ->amount0 = amount0Reque
Improper fee distribution mechanism in the `ILOPool.claim()` function.
Audit: 2024-06-vultisig-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-06-vultisig/blob/main/src/ILOPool.sol#L184-L261


# Vulnerability details

## Impact

Users are unable to claim their assets, resulting in the assets becoming frozen.

## Proof of Concept

Let's follow the claiming process step by step:

1. The user's liquidity is burnt at `L205`.
2. The fees yielded from the user's burnt liquidity in the Uniswap V3 pool are calculated at `L214` and `L220`, and these calculated fees are added to the amounts to be sent to the user at `L230` and `L231`.
3. At `L242`, assets are collected from the Uniswap V3 pool. This includes the underlying assets corresponding to the user's burnt liquidity, as well as ALL accumulated fees in the Uniswap V3 pool.
4. The assets are sent to the user at `L252` and `L253`, and the remaining fees are ALL sent to the `FEE_TAKER` at `L259` and `L260`.

Now, let's consider step 3. This step collects ALL the fees yielded by the entire liquidity in the pool, not just the user's liquidity. These fees are then distributed to the user and the `FEE_TAKER`. As a result, from the second claiming onwards, there may not be enough fees remaining to be sent to the user, potentially leading to the reversal of the transaction.

```solidity
    function claim(uint256 tokenId)
        external
        payable
        override
        isAuthorizedForToken(tokenId)
        returns (uint256 amount0, uint256 amount1)
    {
        // only can claim if the launch is successfully
        require(_launchSucceeded, "PNL");

        // calculate amount of unlocked liquidity for the position
        uint128 liquidity2Claim = _claimableLiquidity(tokenId);
        IUniswapV3Pool pool = IUniswapV3Pool(_cachedUniV3PoolAddress);
        Position storage position = _positions[tokenId];
        {
            IILOManager.Project memory _project = IILOManager(MANAGER).project(address(pool));

            uint128 positionLiquidity = position.liquidity;
            require(positionLiquidity >= liquidity2Claim);

            // get amount of token0 and token1 that pool will return for us
205         (amount0, amount1) = pool.burn(TICK_LOWER, TICK_UPPER, liquidity2Claim);

            // get amount of token0 and token1 after deduct platform fee
            (amount0, amount1) = _deductFees(amount0, amount1, _project.platformFee);

            bytes32 positionKey = PositionKey.compute(address(this), TICK_LOWER, TICK_UPPER);

            // calculate amount of fees that position generated
            (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, , ) = pool.positions(positionKey);
214         uint256 fees0 = FullMath.mulDiv(
                                feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128,
                                positionLiquidity,
                                FixedPoint128.Q128
                            );
            
220         uint256 fees1 = FullMath.mulDiv(
                                feeGrowthInside1Las
The fees for the `FEE_TAKER` are not calculated correctly in the [ILOPool.claim](https://github.com/code-423n4/2024-06-vultisig/blob/0957ff9e50441cd6de6b4f6e28c7ea93f5cffa85/src/ILOPool.sol#L184-L261) function
Audit: 2024-06-vultisig-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-06-vultisig/blob/0957ff9e50441cd6de6b4f6e28c7ea93f5cffa85/src/ILOPool.sol#L184-L261


# Vulnerability details

## Impact
When position owners claim their rewards, fees are sent to the `FEE_TAKER`. However, it is not calculated correctly and larger amount of tokens are sent to the `FEE_TAKER`. As a result, some claimers can't claim their rewards due to lack of tokens in `ILOPool`.

## Proof of Concept
Position owners can claim their rewards by calling the [claim](https://github.com/code-423n4/2024-06-vultisig/blob/0957ff9e50441cd6de6b4f6e28c7ea93f5cffa85/src/ILOPool.sol#L184-L261) function.
The amount of fees that `FEE_TAKER` receives are intended to be the sum of platform fee and protocol fee.
```solidity
                 // get amount of token0 and token1 after deduct platform fee
208              (amount0, amount1) = _deductFees(amount0, amount1, _project.platformFee);
                 ...
                 // amount of fees after deduct performance fee
227              (fees0, fees1) = _deductFees(fees0, fees1, _project.performanceFee);
```
From the codebase, the amount of fees that `FEE_TAKER` receives `amountCollected0-amount0` of `token0` and `amountCollected1-amount1` of `token1`.
```solidity
257          address feeTaker = IILOManager(MANAGER).FEE_TAKER();
258          // transfer fee to fee taker
259          TransferHelper.safeTransfer(_cachedPoolKey.token0, feeTaker, amountCollected0-amount0); //audit-issue 
260          TransferHelper.safeTransfer(_cachedPoolKey.token1, feeTaker, amountCollected1-amount1); //audit-issue 
```
Here, the variables `amountCollected0` and `amountCollected1` represents the collected amount of tokens generated by the position that `ILOPool` holds on `Uniswap V3`.
```solidity
242      (uint128 amountCollected0, uint128 amountCollected1) = pool.collect(
             address(this),
             TICK_LOWER,
             TICK_UPPER,
             type(uint128).max,
             type(uint128).max
         );
```
Since `ILOPool` collects fee from Uniswap V3 with the liquidation of the whole position is used, the amount of fees transferred to the `FEE_TAKER` is larger than the sum of platform fee and performance fee.
As a result, less tokens will remain in `ILOPool` and some claimers can't claim their rewards.

## Tools Used
Manual Review

## Recommended Mitigation Steps
The amount of tokens that is expected to be transferred to the `FEE_TAKER` should be the sum of platform fee and performance fee.








## Assessed type

Other
ILOPool::claim : The amount transfered to the feeTaker is incorrect, which causes the claim function to revert 
Audit: 2024-06-vultisig-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-06-vultisig/blob/cb72b1e9053c02a58d874ff376359a83dc3f0742/src/ILOPool.sol#L259


# Vulnerability details

## Impact

When an eligible user calls the claim function, the following happens:

* the claimable liquidity (liquidity2Claim) is calculated according to the values specified in the vestingConfigs and the curren ttimestamp  
* the burn function is called on the Uniswap pool and the claimable liquidity is burned
* the project platform fee is deducted from the returned T0 and T1 amounts to adjust the amounts that will later be transferred to the msg.caller
* the accumulated Uniswap pool fees are calculated - for the claimable liquidity! 
* the performance fee is deducted from the returned T0 and T1 fee amounts 
* the adjusted fee amounts are added to the previously calculated amounts that will later be transferred to the msg.caller
* the position feeGrowthInside01LastX128 & liquidity values are updated for fee calculations on subsequent claim() calls
* the collect() function is called on the Uniswap pool => this transfer the burned T01 amounts and the TOTAL accumulated fees to the ILOPool !!! 
* the previously calculated TO1 + Fee01 amounts are transferred to the user
* **and finally, the ENTIRE left-over amounts for T01 are transferred to the feeTaker - this is WRONG !!!**

**Important:** When we call collect(), Uniswap transfers all accumulated fees to the ILOPool (not just the amount proportional to the burned liquidity). Proportional fee amounts (according to the burned liquidity) are sent to the user, however, ALL remaining Uniswap fees are sent to the feeTaker:

```
TransferHelper.safeTransfer(_cachedPoolKey.token0, feeTaker, amountCollected0 - amount0);
TransferHelper.safeTransfer(_cachedPoolKey.token1, feeTaker, amountCollected1 - amount1);
```

This means, when other users call the claim() functions and a sufficient amount of fees have been accumulated on the Uniswap pool (token swaps...), not enough tokens will be left on the ILOPool and the following calls will cause a revert:

```
TransferHelper.safeTransfer(_cachedPoolKey.token0, ownerOf(tokenId), amount0);
TransferHelper.safeTransfer(_cachedPoolKey.token1, ownerOf(tokenId), amount1);
```

At this stage, the protocol becomes unusable and users will no longer be able to claim their funds and pool earnings.
 

## Proof of Concept

Add the following code sections to the ILOPool.t.sol file.

We need the ISwapRouter interface. Either install and import the contracts/interfaces/ISwapRouter.sol or define an interface with the required functions at the top of the test file:

```
interface ISwapRouter {
    struct ExactInputSingleParams {
        address tokenIn;
        address tokenOut;
        uint24 fee;
        address recipient;
        uint256 deadline;
        uint256 amountIn;
        uint256 amountOutMinimum;
        uint160 sqrtPriceLimitX96;
    }

    struct ExactOutputSingleParams {
        address tokenIn;
        address to
ILOPool.claim Sends Excess Fees to FEE_TAKER
Audit: 2024-06-vultisig-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-06-vultisig/blob/main/src/ILOPool.sol#L242-L248
https://github.com/code-423n4/2024-06-vultisig/blob/main/src/ILOPool.sol#L257-L260


# Vulnerability details

## Impact

When a user claims their fees from `ILOPool.claim`, they receive their accrued fees up to that point. However, the function then attempts to send the `performance fee` to `feeTaker`, performing a collect on the Uniswap position with the amount `type(uint128).max`, which withdraws all fees from all users. This excess is then sent to feeTaker, causing potential DoS and future payment issues.

## Proof of Concept

ILOPool.sol:

```
(uint128 amountCollected0, uint128 amountCollected1) = pool.collect(
            address(this),
            TICK_LOWER,
            TICK_UPPER,
            type(uint128).max,
            type(uint128).max
        );

...

        address feeTaker = IILOManager(MANAGER).FEE_TAKER();
        // transfer fee to fee taker
        TransferHelper.safeTransfer(_cachedPoolKey.token0, feeTaker, amountCollected0-amount0);
        TransferHelper.safeTransfer(_cachedPoolKey.token1, feeTaker, amountCollected1-amount1);
```

amountCollected{0,1} represent the accumulated fees from all users, so feeTaker is receiving almost all the benefits.

## Tools Used

Foundry

## Recommended Mitigation Steps

The function should send to feeTaker only the fees obtained relative to what the user claimed.


## Assessed type

Uniswap
Investors in an ILOPool can lose out on their share of LP position Fees earned from Uniswap V3
Audit: 2024-06-vultisig-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-06-vultisig/blob/cb72b1e9053c02a58d874ff376359a83dc3f0742/src/ILOPool.sol#L259


# Vulnerability details

After a pool has been launched, investors can call claim to get back the unlocked part of their liquidity along with any LP fees earned on that liquidity until then. 

*This is a complex issue, please read till the end.*

The function first calculates the claimable liquidity of the investor's based on the total liquidity unlocked from all the vesting schedules uptil now. 

liquidity2Claim = _claimableLiquidity()

```
    function _claimableLiquidity(uint256 tokenId) internal view override returns (uint128) {
        uint128 liquidityClaimed = _positionVests[tokenId].totalLiquidity - _positions[tokenId].liquidity;
        uint128 liquidityUnlocked = _unlockedLiquidity(tokenId);
        return liquidityClaimed < liquidityUnlocked ? liquidityUnlocked - liquidityClaimed : 0;
    }
```

Here, _unlockedLiquidity() just returns the total amount of liquidity unlocked for the position since the vesting start uptil now. Then we subtract any liquidity that was already claimed and return the remaining amount as liquidity2Claim. This means this would be a portion of the NFT position(tokenID)'s actual deposited liquidity.

Now back to the execution of claim() function. 

Now it calls the pool.burn function to burn exactly "liquidity2Claim" amount out of the whole position of this particular ILOPool in the uniswap v3 pool.

The burn function can be [seen here](https://github.com/Uniswap/v3-core/blob/d8b1c635c275d2a9450bd6a78f3fa2484fef73eb/contracts/UniswapV3Pool.sol#L517). 

It modifies the position and does a lot of operations to update the position info and returns amount0 and amount1 : the amount of token0 and token1 obtained from burning exactly "liquidity2Claim" out of the whole aggregated liquidity. These amounts are then added to tokensOwed0 and tokensOwed1, and they can be collected later. 

What is relevant to the issue is this particular flow : burn() => [_modifyPosition()](https://github.com/Uniswap/v3-core/blob/d8b1c635c275d2a9450bd6a78f3fa2484fef73eb/contracts/UniswapV3Pool.sol#L319) => [_updatePosition()](https://github.com/Uniswap/v3-core/blob/d8b1c635c275d2a9450bd6a78f3fa2484fef73eb/contracts/UniswapV3Pool.sol#L379) => [position.update()](https://github.com/Uniswap/v3-core/blob/d8b1c635c275d2a9450bd6a78f3fa2484fef73eb/contracts/UniswapV3Pool.sol#L442) => [Position Library](https://github.com/Uniswap/v3-core/blob/d8b1c635c275d2a9450bd6a78f3fa2484fef73eb/contracts/libraries/Position.sol#L44)

Lets have a look at the logic in position.update :

```
   function update(
        Info storage self,
        int128 liquidityDelta,
        uint256 feeGrowthInside0X128,
        uint256 feeGrowthInside1X128
    ) internal {
        Info memory _self = self;

        uint128 liquidityNext;
        if (liquidityDelta == 0) {
            require(_self.liquidity > 0, 'NP'); // disallow pokes for 0 liquidi
`claim` function incorrectly assumes that `amount0` and `amount1` returned by `UniswapV3Pool.burn` function exclude swap fees that LP is entitled to
Audit: 2024-06-vultisig-findings
Severity: HIGH

# Lines of code

https://github.com/Uniswap/v3-core/blob/main/contracts/interfaces/pool/IUniswapV3PoolActions.sol#L51-L63
https://github.com/Uniswap/v3-core/blob/main/contracts/UniswapV3Pool.sol#L517-L543
https://github.com/Uniswap/v3-core/blob/main/contracts/interfaces/pool/IUniswapV3PoolActions.sol#L31-L49
https://github.com/Uniswap/v3-core/blob/main/contracts/UniswapV3Pool.sol#L490-L513
https://github.com/code-423n4/2024-06-vultisig/blob/58ebda57ccf6a74bdef2b88eb18a62ec4ad46112/src/ILOPool.sol#L184-L261


# Vulnerability details

## Impact
`amount0` and `amount1` returned by the following `UniswapV3Pool.burn` function would include swap fees that the LP is entitled to. The reasons are as follows:
- The `UniswapV3Pool.burn` function's comment states: `Can be used to trigger a recalculation of fees owed to a position by calling with an amount of 0`. When the LP has accumulated some swap fees over time, he can call the `UniswapV3Pool.burn` function with 0 as the `amount` input. In this case, `amount0` and/or `amount1` are/is such positive swap fees, and incrementing `position.tokensOwed0` and/or `position.tokensOwed1` by `amount0` and/or `amount1` causes `position.tokensOwed0` and/or `position.tokensOwed1` to include such swap fees.
- The comment of the `UniswapV3Pool.collect` function below states: `Does not recompute fees earned, which must be done either via mint or burn of any amount of liquidity`. When the `UniswapV3Pool.collect` function is called, parts or all of `position.tokensOwed0` and `position.tokensOwed1` are transferred to `recipient`. After the LP burns 0 `amount`, his recipient can still receive the accumulated swap fees that he is entitled to because `position.tokensOwed0` and `position.tokensOwed1` were incremented by the `amount0` and `amount1`, which include such positive swap fees, in his `UniswapV3Pool.burn` function call.

https://github.com/Uniswap/v3-core/blob/main/contracts/interfaces/pool/IUniswapV3PoolActions.sol#L51-L63
```solidity
    /// @notice Burn liquidity from the sender and account tokens owed for the liquidity to the position
    /// @dev Can be used to trigger a recalculation of fees owed to a position by calling with an amount of 0
    /// @dev Fees must be collected separately via a call to #collect
    /// @param tickLower The lower tick of the position for which to burn liquidity
    /// @param tickUpper The upper tick of the position for which to burn liquidity
    /// @param amount How much liquidity to burn
    /// @return amount0 The amount of token0 sent to the recipient
    /// @return amount1 The amount of token1 sent to the recipient
    function burn(
        int24 tickLower,
        int24 tickUpper,
        uint128 amount
    ) external returns (uint256 amount0, uint256 amount1);
```

https://github.com/Uniswap/v3-core/blob/main/contracts/UniswapV3Pool.sol#L517-L543
```solidity
    function burn(
        int24 tickLower,
        int24 tickUpper,
        uint128 amount
    ) external override l
Any subsequent `claim()` after first `claim()` will receive incorrect amount of tokens even could lead to revert operation
Audit: 2024-06-vultisig-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-06-vultisig/blob/cb72b1e9053c02a58d874ff376359a83dc3f0742/src/ILOPool.sol#L242-L248


# Vulnerability details

# Summary
Function `ILOPool.claim()` will collect the token owed in uniswap pool. The token owed includes fees collected for the global liquidity (liquidity provided by `ILOPool`). The problem is all the fees will be collected at the first time when the `ILOPool.claim()` function is called.

```
(uint128 amountCollected0, uint128 amountCollected1) = pool.collect(
    address(this),
    TICK_LOWER,
    TICK_UPPER,
    type(uint128).max,
    type(uint128).max
);
```

Because all the fees are collected at the same time, the remaining fees will be sent to `FEE_TAKER` after deduction the amount received by claimer from amount collected.

The principal amount the user will receive calculated as : `principal amount in uniswap position * (1 - platform fee)`

The amount of fees collected for the given user is calculated as : `liquidity owned by user * (current gowth inside - last growth inside) * (1 - performance fee)`

The amount received by user : `principal amount + fees collected`

The amount received by `FEE_TAKER` : `amount collected from uniswap - amount received by user`.

The problem here is the amount received by `FEE_TAKER` contains fees that must be received by other users.

Assume there is no trading activity in the pool for the next few hours. The second user call `claim()` function.

Because there is no trading activity, the `current growth inside` will not change. Consequently, the `amount collected from uniswap` (`amountCollected0` and `amountCollected1`) will be same as `principal amount in uniswap position` (it is because the fees collected from uniswap was already collected by previous `claim()` called by first user).

Now, two condition might happen.

First, `amount collected from uniswap >= amount received by user`. If this is the case, the user still receive the tokens but lose fees.

Second, `amount collected from uniswap < amount received by user`. If this is the case, the operation will revert.

# Impact
The investor can lose fees, even could lead to revert operation.

# Tools used
Manual Analysis

# Recommended Mitigation
Only collect tokens proportionate to liquidity of the investor.

```diff
...
+ uint256 principalAmount0;
+ uint256 principalAmount1;
+ (principalAmount0, principalAmount1) = pool.burn(TICK_LOWER, TICK_UPPER, liquidity2Claim);

+uint256 feesOwed0 = FullMath.mulDiv(
+   feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128,
+   positionLiquidity,
+   FixedPoint128.Q128
+);

+uint256 feesOwed1 = FullMath.mulDiv(
+   feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128,
+   positionLiquidity,
+   FixedPoint128.Q128
+);
...
(uint128 amountCollected0, uint128 amountCollected1) = pool.collect(
    address(this),
    TICK_LOWER,
    TICK_UPPER,
-   type(uint128).max,
+   principalAmount0 + feesOwed0
-   type(uint128).max
+   principalAmount1 +