xanoutas/solidity-security-findings huggingface dataset:
Missing Slippage Protection in Claim Function
Audit: 2024-06-vultisig-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
The absence of slippage protection in the claim function can lead to users receiving significantly less tokens than expected due to price changes during the transaction. This can result in financial losses for users and affect their trust in the platform.

```javascript
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
            (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);
            uint256 fees0 = FullMath.mulDiv(
                                feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128,
                                positionLiquidity,
                                FixedPoint128.Q128
                            );
            
            uint256 fees1 = FullMath.mulDiv(
                                feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128,
                                positionLiquidity,
                                FixedPoint128.Q128
                            );

            // amount of fees after deduct performance fee
            (fees0, fees1) = _deductFees(fees0, fees1, _project.performanceFee);

            // fees is combined with liquidity token amount to return to the user
            amount0 += fees0;
            amount1 += fees1;

            position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128;
            position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128;

            // subtraction is safe because we checked positionLiquidity is gte liquidity2Claim
            position.liquidity = positionLiquidity - liquidity2Claim;
            emit DecreaseLiquidity(tokenId, liquidity2Claim, amount0, amount1);
Using slot0 to get a price can lead to a price manipulation
Audit: 2024-06-vultisig-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-06-vultisig/blob/main/src/ILOManager.sol#L189


# Vulnerability details

## Impact

In `ILOManager` smart contract, when the project is about to be launched with all the pools, we use `slot0` to get the value of `sqrtPrice` which can lead to price manipulations as the `slot0` param represents the current price.

## Proof of Concept

In the `ILOManager` smart contract, `launch()` has the following functionality to get the price:

https://github.com/code-423n4/2024-06-vultisig/blob/main/src/ILOManager.sol#L189
```
(uint160 sqrtPriceX96, , , , , , ) = IUniswapV3Pool(uniV3PoolAddress).slot0();
```

So when the project is launched, we use the current price instead of using the time weighted price which can lead to different flash loan manipulations by performing large value swaps.

## Tools Used

Manual review.

## Recommended Mitigation Steps

Consider relying on time weighted price to avoid flash loan manipulations.


## Assessed type

Other
Missing slippage protection for the `buy()` function
Audit: 2024-06-vultisig-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

In the current implementation of the `buy()` function inside of the `ILOPool` smart contract, the liquidity for the position is changed based on the change of `liquidityDelta` parameter from the `getLiquidityForAmount0()` or `getLiquidityForAmount1()` function calls. However, the function `buy()` only checks whether `liquidityDelta` > 0 and misses minimum amount parameter for slippage protection.

## Proof of Concept

Let's take a look at the `buy()` function and how liquidity changes:

https://github.com/code-423n4/2024-06-vultisig/blob/main/src/ILOPool.sol#L155-159
```
  if (RAISE_TOKEN == _cachedPoolKey.token0) {
            liquidityDelta = LiquidityAmounts.getLiquidityForAmount0(SQRT_RATIO_X96, SQRT_RATIO_UPPER_X96, raiseAmount);
        } else {
            liquidityDelta = LiquidityAmounts.getLiquidityForAmount1(SQRT_RATIO_LOWER_X96, SQRT_RATIO_X96, raiseAmount);
        }
```

So we change the liquidity delta via `getLiquidityForAmount0()` and `getLiquidityForAmount1()` based on what is the raise token. After that there is a following check:


https://github.com/code-423n4/2024-06-vultisig/blob/main/src/ILOPool.sol#L161
```
 require(liquidityDelta > 0, "ZA");
```
And after that the liquidity delta is added to the position:

https://github.com/code-423n4/2024-06-vultisig/blob/main/src/ILOPool.sol#L167
```
_position.liquidity += liquidityDelta
```

So there is no any minimum parameter for `liquidityDelta` that is supplied by the caller of the function and therefore it opens up an attack vector where desired amount of liquidity delta cannot be added due to manipulations by other users via front-running.

## Tools Used

Manual review.

## Recommended Mitigation Steps

Add minimum liquidity delta parameter to avoid front-running and liquidity manipulation.


## Assessed type

Other
If one pool gets refunded, all the cloned pools will not be able to launch
Audit: 2024-06-vultisig-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-06-vultisig/blob/cb72b1e9053c02a58d874ff376359a83dc3f0742/src/ILOManager.sol#L187-L198


# Vulnerability details

## Impact

All the pools cannot be launched because of one pool.

## Proof of Concept

When a protocol creates a project, he can create many pools for the project through `ILOManager.initILOPool()`. One reason for creating many pools for the ILO is to have different tick sizes or different hard/soft cap and start/end parameters.

The cloned pool address will be saved in the `_initializedILOPools` mapping.

```
        IILOPool.InitPoolParams memory initParams = IILOPool.InitPoolParams({
            uniV3Pool: params.uniV3Pool,
            tickLower: params.tickLower,
            tickUpper: params.tickUpper,
            sqrtRatioLowerX96: sqrtRatioLowerX96,
            sqrtRatioUpperX96: sqrtRatioUpperX96,
            hardCap: params.hardCap,
            softCap: params.softCap,
            maxCapPerUser: params.maxCapPerUser,
            start: params.start,
            end: params.end,
            vestingConfigs: params.vestingConfigs
        });
        IILOPool(iloPoolAddress).initialize(initParams);
>       _initializedILOPools[params.uniV3Pool].push(iloPoolAddress);
```

When it is time for the protocol to launch the pools, the admin will call `launch()`, and the function loops through all the pools that was created and attempt to launch the pool.

```
        for (uint256 i = 0; i < initializedPools.length; i++) {
>           IILOPool(initializedPools[i]).launch();
        }
```

In ILOPool, if there is a refund in one of the pool, the pool will not be able to launch. Since there is a loop, if one pool fails to launch, all the pool will fail to launch.

```
    function launch() external override OnlyManager() {
        require(!_launchSucceeded, "PL");
        // when refund triggered, we can not launch pool anymore
        require(!_refundTriggered, "IRF");
        // make sure that soft cap requirement match
        require(totalRaised >= saleInfo.softCap, "SC");
```

There are also other reasons for the failure to launch, such as the `totalRaised` being lesser than the `saleInfo.softCap`.

## Tools Used

Manual Review

## Recommended Mitigation Steps

Have a try/catch block when creating pools so that those pools which are ready for launch will be launched, and those pools which are not ready to launch will be able to get refunds.


## Assessed type

Loop
DOS Attacker can frontrun `ILOManager.launch()` to make it revert with swap because of strict equality check of spot price.
Audit: 2024-06-vultisig-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-06-vultisig/blob/main/src/ILOManager.sol#L190


# Vulnerability details

## Impact
DoS revert while launching ILOPools.

## Proof of Concept
There's strict equality check between initial spot price and current spot price in `ILOManager.launch()`.

Attacker can frontrun swap tx to manipulate spot price and subsequent launch tx to be reverted.

https://github.com/code-423n4/2024-06-vultisig/blob/main/src/ILOManager.sol#L187-L198

Strict 
```
    function launch(address uniV3PoolAddress) external override {
...
        (uint160 sqrtPriceX96, , , , , , ) = IUniswapV3Pool(uniV3PoolAddress).slot0();
        require(_cachedProject[uniV3PoolAddress].initialPoolPriceX96 == sqrtPriceX96, "UV3P"); // @audit - launch can be DoSed by frontrunning swap
...
        }

        emit ProjectLaunch(uniV3PoolAddress);
    }
```

## Tools Used
Manual Review

## Recommended Mitigation Steps
1) It can be mitigated by hiding tx with Flashbot RPC.

2) Strict equality can be changed to greater or equality check with minimum spot price.


## Assessed type

DoS
maxCapPerUser should check the total raiseAmount of the positions owned by a user
Audit: 2024-06-vultisig-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
__maxCapPerUser__ should check the total raiseAmount of the ILO positions owned by a user, or a user can buy many ILO positions with the total raiseAmount higher than __maxCapPerUser__.

## Proof of Concept
[__maxCapPerUser__](https://github.com/code-423n4/2024-06-vultisig/blob/cb72b1e9053c02a58d874ff376359a83dc3f0742/src/interfaces/IILOSale.sol#L9) is used in ILOPool contract to limit the max raiseAmount a single user can buy.
```
    function buy(uint256 raiseAmount, address recipient)
        external override 
        returns (
            uint256 tokenId,
            uint128 liquidityDelta
        )
    {
        ...

        // if investor already have a position, just increase raise amount and liquidity
        // otherwise, mint new nft for investor and assign vesting schedules
        if (balanceOf(recipient) == 0) {
@>          _mint(recipient, (tokenId = _nextId++));
            _positionVests[tokenId].schedule = _vestingConfigs[0].schedule;
        } else {
@>          tokenId = tokenOfOwnerByIndex(recipient, 0);
        }


        Position storage _position = _positions[tokenId];
@>      require(raiseAmount <= saleInfo.maxCapPerUser - _position.raiseAmount, "UC");

        ...
    }
```
Protocol compares __maxCapPerUser__ to the raiseAmount of the first position the user owned, to check if __maxCapPerUser__ is exceeded. 
The problem is that the position token can be transferred and a user can own multiple positions, results in  the total raiseAmount of the positions owned by a user is larger than __maxCapPerUser__.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Should check if the total raiseAmount of the ILO positions owned by a user larger than __maxCapPerUser__.
```diff
    function buy(uint256 raiseAmount, address recipient)
        external override 
        returns (
            uint256 tokenId,
            uint128 liquidityDelta
        )
    {
        ...

        // if investor already have a position, just increase raise amount and liquidity
        // otherwise, mint new nft for investor and assign vesting schedules
+       uint256 totalRaiseAmount;
        if (balanceOf(recipient) == 0) {
            _mint(recipient, (tokenId = _nextId++));
            _positionVests[tokenId].schedule = _vestingConfigs[0].schedule;
        } else {
            tokenId = tokenOfOwnerByIndex(recipient, 0);
+           for (uint i; i < balanceOf(recipient); ++i) {
+               uint256 _tokenId = tokenOfOwnerByIndex(recipient, i);
+               totalRaiseAmount += _positions[_tokenId].raiseAmount;
+           }
        }

        Position storage _position = _positions[tokenId];
-       require(raiseAmount <= saleInfo.maxCapPerUser - _position.raiseAmount, "UC");
+       require(raiseAmount <= saleInfo.maxCapPerUser - totalRaiseAmount, "UC");

        ...
Project initialization is likely to fail due to `slot0` manipulation
Audit: 2024-06-vultisig-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
The `slot0` of a Uniswap V3 pool, which contains the current price (`sqrtPriceX96Existing`), can be easily manipulated.
This means that the `sqrtPriceX96Existing` fetched may frequently differ from the `params.initialPoolPriceX96` provided by the caller of `initProject()`.

Due to the manipulability of `slot0`, the function `_initUniV3PoolIfNecessary()` may revert almost every time if the existing pool's price has changed from the initial price provided.
This makes it difficult to initialize projects reliably.

## Vulnerability Details
During project initialization via [initProject()](https://github.com/code-423n4/2024-06-vultisig/blob/cb72b1e9053c02a58d874ff376359a83dc3f0742/src/ILOManager.sol#L61), the `uniV3PoolAddress` is fetched by calling `_initUniV3PoolIfNecessary()` with `params.initialPoolPriceX96` supplied by the caller.
```solidity
uniV3PoolAddress = _initUniV3PoolIfNecessary(poolKey, params.initialPoolPriceX96);
``` 

The function [_initUniV3PoolIfNecessary()](https://github.com/code-423n4/2024-06-vultisig/blob/cb72b1e9053c02a58d874ff376359a83dc3f0742/src/ILOManager.sol#L111-L121) checks if the Uniswap V3 pool already exists. If it does, it verifies that the existing pool's initial price (`sqrtPriceX96Existing`) matches the supplied `params.initialPoolPriceX96`. If there is a mismatch, the function reverts with an error.
```solidity
if (pool == address(0)) {
    //...
} else {
>>  (uint160 sqrtPriceX96Existing, , , , , , ) = IUniswapV3Pool(pool).slot0();
    if (sqrtPriceX96Existing == 0) {
        IUniswapV3Pool(pool).initialize(sqrtPriceX96);
    } else {
>>      require(sqrtPriceX96Existing == sqrtPriceX96, "UV3P"); //@audit-issue Might always revert
    }
}
```

However, due to the manipulability of `slot0`, the function `_initUniV3PoolIfNecessary()` may revert almost every time if the existing pool's price has changed from the initial price provided.

## Tools Used
Manual Review

## Recommended mitigation steps
- Remove the requirement that `sqrtPriceX96Existing` must match `params.initialPoolPriceX96`.

This will prevent the function from reverting due to minor price differences caused by market fluctuations.
- Implement a tolerance range for price validation.

Allow a small percentage deviation between `sqrtPriceX96Existing` and `params.initialPoolPriceX96`. This accommodates minor price changes while still ensuring the pool is initialized at a reasonable price.


## Assessed type

Uniswap
The whitelisted addresses can `buy()` and call `transferFrom()` many times, it let non whitelisted addresses to own liquidity offering in ILOPool
Audit: 2024-06-vultisig-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-06-vultisig/blob/cb72b1e9053c02a58d874ff376359a83dc3f0742/src/ILOPool.sol#L25
https://github.com/code-423n4/2024-06-vultisig/blob/cb72b1e9053c02a58d874ff376359a83dc3f0742/src/ILOPool.sol#L133
https://github.com/code-423n4/2024-06-vultisig/blob/cb72b1e9053c02a58d874ff376359a83dc3f0742/src/ILOPool.sol#L151
https://github.com/code-423n4/2024-06-vultisig/blob/cb72b1e9053c02a58d874ff376359a83dc3f0742/src/ILOPool.sol#L136


# Vulnerability details

# Summary
`ILOPool` contract inherited the oz `ERC721` contract [here](https://github.com/code-423n4/2024-06-vultisig/blob/cb72b1e9053c02a58d874ff376359a83dc3f0742/src/ILOPool.sol#L25). Consequently, the `ILOPool` contract will inherit `transferFrom()` function. The `buy()` function will check `recipient` address whether the address is in the whitelist or not [here](https://github.com/code-423n4/2024-06-vultisig/blob/cb72b1e9053c02a58d874ff376359a83dc3f0742/src/ILOPool.sol#L133). Besides that, each `recipient` is only able to buy at maximum [`saleInfo.maxCapPerUser`](https://github.com/code-423n4/2024-06-vultisig/blob/cb72b1e9053c02a58d874ff376359a83dc3f0742/src/ILOPool.sol#L151):


```
require(raiseAmount <= saleInfo.maxCapPerUser - _position.raiseAmount, "UC");
```

But, any whitelisted addresses can bypass this restriction by buying and transferring the token they own to any addresses they can control as much as they want. If the attackers have enough money, they can `buy()` and call `transferFrom()` multiple times until the [`saleInfo.hardCap`](https://github.com/code-423n4/2024-06-vultisig/blob/cb72b1e9053c02a58d874ff376359a83dc3f0742/src/ILOPool.sol#L136) reached. This will prevent other from buying the liquidity offering.

# Impact
Attackers can dominate the liquidity offering allowing them to control the market.

# Proof of Concept
Here is the proof of concept:

Add the code below to `ILOPoolTest` contract in `test/ILOPool.t.sol`

```solidity
function testBuyAndTransfer() external {
    address receiver1 = makeAddr("receiver1");
    address receiver2 = makeAddr("receiver2");
    uint256 raiseAmount = 50000 ether; // use 50000 ether two times because the hardcap is 100000 ether

    // populate balance for receiver1 to buy liquidity offering
    vm.prank(receiver1);
    IERC20(USDC).approve(iloPool, 1000000000 ether);
    _writeTokenBalance(USDC, receiver1, 1000000000 ether);
    assertEq(IERC20(USDC).balanceOf(receiver1), 1000000000 ether);

    // populate balance for receiver2 to buy liquidity offering
    vm.prank(receiver2);
    IERC20(USDC).approve(iloPool, 1000000000 ether);
    _writeTokenBalance(USDC, receiver2, 1000000000 ether);
    assertEq(IERC20(USDC).balanceOf(receiver2), 1000000000 ether);

    // receiver1 try to buy but must fail because it is not whitelisted
    vm.expectRevert(bytes("UA"));
    _buy(SALE_START+1, raiseAmount);

    // receiver2 must fail too
    vm.expectRevert(bytes("UA"));
    _buy(SALE_START+1, raiseAmount);
No slippage protection for function `buy`.
Audit: 2024-06-vultisig-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Users will suffer losses due to slippage.

## Detail
In the function `buy`, user can transfer fund into contract and obtain some liquidity. However, there is no slippage check for `liquidityDelta`, causing user may receive less liquidity than expected.

## Tools Used
VScode

## Recommended Mitigation Steps
Add a minLiquidity parameter to the function `buy`.


## Assessed type

Context
No slippage protection for the function `claim`
Audit: 2024-06-vultisig-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Due to slippage, users may receive fewer tokens than expected.

## Proof of Concept
Function `claim` will burn users' liquidity and return some tokens(including token0 and token1). However, the returned amount is not checked, which may lead to user receive fewer tokens than expected.

## Tools Used
VScode

## Recommended Mitigation Steps
Add slippage protect for amount0 and amount1.


## Assessed type

Context
Full liquidity will not vest even any one vest.start is greater than block.timestamp
Audit: 2024-06-vultisig-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
_unlockedLiquidity() function checks the unlocked liquidity by iterating over all the vesting schedules
```solidity
 function _unlockedLiquidity(uint256 tokenId) internal view override returns (uint128 liquidityUnlocked) {
        ....

     --->   for (uint256 index = 0; index < vestingSchedule.length; index++) {

            LinearVest storage vest = vestingSchedule[index];

            // if vest is not started, skip this vest and all following vest
        --->    if (block.timestamp < vest.start) {
                break;
            }

           ....
        }
    }
```
It assumes all the vesting schedule will be in increasing start time but this may not be the case.
Eg: it is possible that a vesting schedule is started first but in loop it comes later, which will be issue because that vested amount will not be included in liquidityUnlocked

Reason:- it uses break but it should use continue

## Proof of Concept
https://github.com/code-423n4/2024-06-vultisig/blob/cb72b1e9053c02a58d874ff376359a83dc3f0742/src/ILOPool.sol#L400C4-L431C6

## Tools Used
VS code

## Recommended Mitigation Steps
Use continue while checking for vest.start


## Assessed type

Error
Using balanceOf() for validation is wrong 
Audit: 2024-06-vultisig-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
While buying liquidity, buy() checks the balanceOf(recipient) to check if recepient already have any position or not. But a malicious user can transfer his nft to different address and again buy liquidity
```solidity
  // if investor already have a position, just increase raise amount and liquidity
        // otherwise, mint new nft for investor and assign vesting schedules
   --->     if (balanceOf(recipient) == 0) {
            _mint(recipient, (tokenId = _nextId++));
            _positionVests[tokenId].schedule = _vestingConfigs[0].schedule;
        } else {
            tokenId = tokenOfOwnerByIndex(recipient, 0);
        }
```

## Proof of Concept
https://github.com/code-423n4/2024-06-vultisig/blob/cb72b1e9053c02a58d874ff376359a83dc3f0742/src/ILOPool.sol#L141C7-L148C10

## Tools Used
VS code

## Recommended Mitigation Steps
Use mapping to store minted nft and for verification


## Assessed type

ERC721