xanoutas/solidity-security-findings huggingface dataset:
Use of slot0 is easy to manipulate
Audit: 2024-06-vultisig-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
slot0 is the most recent data point and is therefore extremely easy to manipulate. The protocol uses slot0 to fetch the prices when initialising a [uniswap pool](https://github.com/code-423n4/2024-06-vultisig/blob/cb72b1e9053c02a58d874ff376359a83dc3f0742/src/ILOManager.sol#L61)
or [launching a pool](https://github.com/code-423n4/2024-06-vultisig/blob/cb72b1e9053c02a58d874ff376359a83dc3f0742/src/ILOManager.sol#L189)

## Proof of Concept
sqrtPriceX96Existing is pulled from Uniswap.slot0, which is the most recent data point and can be manipulated easily via MEV bots and Flashloans with sandwich attacks; which can cause the loss of funds when interacting with the Uniswap.

An attacker can simply manipulate the `sqrtPriceX96Existing` and the token will be bought at a higher price and the attacker would run the transaction to sell; thereby earning gains but causing a loss of funds to whoever called those functions.

## Tools Used
Uniswap

## Recommended Mitigation Steps
Use the TWAP to fetch price instead





## Assessed type

Uniswap
Missing slippage/deadline checks in claim function in ILOPool
Audit: 2024-06-vultisig-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details


## Impact
Investors can lose their funds if the slippage/deadline is not checked in the claim function.
There is no protection such as a minimum return amount or deadline.
This lack of validation can lead to the claim function being sandwich attacked, resulting in the loss of user funds.

## Proof of Concept

```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
            (amount0, amount1) = pool.burn(TICK_LOWER, TICK_UPPER, liquidity2Claim); // @audit missing slippage check

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

## Tools Used
Manual review

## Recommended Mitigation Steps
It's recommended to add the slippage/deadline checks in the claim functions to prevent the loss of funds for the user.



## Assessed type

MEV
When one of the ILOPool launch functions revert, the ILOManager launch function should not revert
Audit: 2024-06-vultisig-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details


## Impact
There are many cases where the `ILOPool` launch functions can revert, such as when the `refundTriggered` is true or the `totalRaise` is less than the softCap.
If one of the `ILOPool` launch fails, the project launch will revert as well even if the other `ILOPools` are successful.

## Proof of Concept
Project admin can create several ILOPools by using [initILOPool](https://github.com/code-423n4/2024-06-vultisig/blob/main/src/ILOManager.sol#L72).
- If one of the `ILOPool` does not meet the `softCap`, the launch function will revert in the `ILOPool` contract. [launch](https://github.com/code-423n4/2024-06-vultisig/blob/main/src/ILOPool.sol#L274)
- After the `refundDeadline` passes, the investor can call the `claimRefund` function in the `ILOPool` contract. [claimRefund](https://github.com/code-423n4/2024-06-vultisig/blob/main/src/ILOPool.sol#L350)
If the `refundTriggered` or the `totalRaise` is less than the `softCap`, the launch function will revert in the `ILOPool` contract. [launch](https://github.com/code-423n4/2024-06-vultisig/blob/main/src/ILOPool.sol#L272-L274)

If one of the `ILOPool` launch functions fails, it will lead to reverts on the `ILOManager` launch function.
```solidity
    function launch(address uniV3PoolAddress) external override {
        require(block.timestamp > _cachedProject[uniV3PoolAddress].launchTime, "LT");
        (uint160 sqrtPriceX96, , , , , , ) = IUniswapV3Pool(uniV3PoolAddress).slot0();
        require(_cachedProject[uniV3PoolAddress].initialPoolPriceX96 == sqrtPriceX96, "UV3P");
        address[] memory initializedPools = _initializedILOPools[uniV3PoolAddress];
        require(initializedPools.length > 0, "NP");
        for (uint256 i = 0; i < initializedPools.length; i++) {
            IILOPool(initializedPools[i]).launch(); // @audit revert if one of the ILOPool launch functions fails
        }
        emit ProjectLaunch(uniV3PoolAddress);
    }
```
The worst-case scenario is that the launch function in the `ILOManager` contract will revert always if one of the `ILOPool` launch functions fails.


## Tools Used
Manual review

## Recommended Mitigation Steps
To enhance the functionality, it is recommended to make changes to the launch function in the `ILOPool` contract so that it returns a status code instead of triggering a revert.
And, the launch function in the `ILOManager` contract should only proceed if at least one of the `ILOPool` launch functions is successful.


## Assessed type

Context
ILOPool can be blocked from ever launching if sqrtPriceX96 changes after launch
Audit: 2024-06-vultisig-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-06-vultisig/blob/cb72b1e9053c02a58d874ff376359a83dc3f0742/src/ILOManager.sol#L190
https://github.com/code-423n4/2024-06-vultisig/blob/cb72b1e9053c02a58d874ff376359a83dc3f0742/src/ILOPool.sol#L269-L335


# Vulnerability details

## Impact
A pool can be blocked from ever launching trapping all funds from users until the `refundDeadline` restriction is met due to an optimistic check as seen below.

```solidity
        (uint160 sqrtPriceX96, , , , , , ) = IUniswapV3Pool(uniV3PoolAddress).slot0();
        require(_cachedProject[uniV3PoolAddress].initialPoolPriceX96 == sqrtPriceX96, "UV3P");
```

This check is unsafe for several reasons. Firstly because this assumes that all `ILOPool` contracts will be for new pools, however the `ILOManager` allows creation of pools that could be already trading. If any user makes a trade on the pool and the price changes, unless the original `sqrtPriceX96` is reach again the pool will be unable to launch and all users funds will be trapped.

Secondly if a user wants to use this pool to bootstrap liquidity for their new protocol, if any user comes across tokens, for example from an airdrop or past sale. They can now add liquidity directly through Uniswap V3, and make trades to change the price of the `sqrtPriceX96` to effectively lock all raised funds in the contract indefinetly.


Important to note that because of [this](https://github.com/code-423n4/2024-06-vultisig/blob/cb72b1e9053c02a58d874ff376359a83dc3f0742/src/ILOManager.sol#L90) check as seen below in `initILOPool()` a pool can also be prevented from ever deploying if this value falls below `sqrtRatioLowerX96`.

```solidity
require(sqrtRatioLowerX96 < _project.initialPoolPriceX96 && sqrtRatioLowerX96 < sqrtRatioUpperX96, "RANGE");
```


## Proof of Concept
```solidity
contract Base is Test {
    // Constants from mainnet
    uint256 internal constant _MAINNET_FORK_BLOCK = 20_076_176;
    address public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
    address public constant RANDOM_TOKEN = 0x2541A36BE4cD39286ED61a3E6AFC2307602489d6;
    ISwapRouter public constant UNISWAP_V3_ROUTER = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564);
    IUniswapFactory public constant UNISWAP_V3_FACTORY = IUniswapFactory(0x1F98431c8aD98523631AE4a59f267346ea31F984);
    IUniswapPool public pool;


    // Mock addresses
    ILOManager public iloManager;
    ILOPool public iloPool;

    address owner = makeAddr('owner');
    address trader = makeAddr('trader');

    function setUp() external {
      vm.createSelectFork('https://rpc.ankr.com/eth', _MAINNET_FORK_BLOCK);

      // Setup vultisig state
      vm.startPrank(owner, owner);
        iloManager = new ILOManager();
        iloPool = new ILOPool();

        uint256 launchTime = block.timestamp + 100;

        pool = IUniswapPool(UNISWAP_V3_FACTORY.getPool(WETH, RANDOM_TOKEN, 3000));

        (uint160 initalSqrtPriceX96, , , , , , ) = IUniswapPool(pool).s
Maximum cap per user can be bypassed in a whitelist-only ILO
Audit: 2024-06-vultisig-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
- Malicious users can bypass maximum cap per user set by project's admin.  
## Description
Project's owner can set a maximum cap for each user to buy in a position.  

In an open-to-all raising pool, it is practically impossible to enforce the maximum cap for each user because they can set a recipient to an unlimited number of EOAs that they own.  

In contrast, in whitelist-only raising pool, it should be expected that this safeguard works as you can limit the number of allowed recipient in a list.  

The maximum cap per user is being enforced in `buy` function.  
See: [ILOPool.sol#L126C14-L152](https://github.com/code-423n4/2024-06-vultisig/blob/main/src/ILOPool.sol#L126-L152)  
```solidity
function buy(uint256 raiseAmount, address recipient)
        external override 
        returns (
            uint256 tokenId,
            uint128 liquidityDelta
        )
    {
    @> require(_isWhitelisted(recipient), "UA");
    require(block.timestamp > saleInfo.start && block.timestamp < saleInfo.end, "ST");
    // check if raise amount over capacity
    require(saleInfo.hardCap - totalRaised >= raiseAmount, "HC");
    totalRaised += raiseAmount;

    require(totalSold() <= saleInfo.maxSaleAmount, "SA");

    // 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");
    @> _position.raiseAmount += raiseAmount;
    ...
    ...
    ...
```
Here is how the mechanism works: 
- Checks `recipient` in whitelist
- If `recipient` already has position, it retrieves position's information from mapping variable.  
- Checks whether a total new raiseAmount exceeds maximum capacity for each user.  

This is the reason that this mechanism doesn't work in open-to-all ILO.  

However, it is not an effective measure in whitelist-only ILO too.  

As the position is hold in a form of NFT, and there is not check against whitelist within ERC721 layer.  

Anyone can bypass this maximum capacity by transferring their NFT to another address, and continue buying more position.  

## Proof-of-Concept
The test below does the following:  
- Init a new project.  
- Init a new pool with max cap at 20_000 ether.  
- Whitelist `INVESTOR` address  
- Demonstrate that max cap for each user can be bypassed by transferring NFT to another account not in the whitelist.  

### Steps
- 1. Create a new test file on `2024-06-vultisig/test/ILOPool.whitelist.t.sol` with provide code below  
- 2. Run `forg
[M-04] - Hard-coded 5% slippage isn't optimal
Audit: 2024-06-vultisig-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-06-vultisig/blob/cb72b1e9053c02a58d874ff376359a83dc3f0742/hardhat-vultisig/contracts/oracles/uniswap/UniswapV3Oracle.sol#L45


# Vulnerability details

##Vulnerability Details
The `peek` function in the `UniswapV3Oracle` hard-codes a 5% slippage protection, which may not optimally adapt to various market conditions such as high volatility or liquidity imbalances.

## Impact
The primary concern is the potential for inaccuracies in price estimations due to the fixed 5% slippage protection, which could lead to for example reduced market efficiency.

## Proof of Concept
https://github.com/code-423n4/2024-06-vultisig/blob/cb72b1e9053c02a58d874ff376359a83dc3f0742/hardhat-vultisig/contracts/oracles/uniswap/UniswapV3Oracle.sol#L45

## Tools Used
Manual review

## Recommended Mitigation Steps
There are 2 ways to mitigate this issue. First workaround is to implement a slippage protection that is set by the operator/manager of the contract and the second more preferable way is to allow users to determine the maximum slippage they are willing to take. Thic can be further implemented on the front-end.

The latter approach is the better choice since the market could change fast before the operator can apply the necessary slippage changes.


## Assessed type

Uniswap
UniswapV3 `slot0` should not be used as it can be manipulated.
Audit: 2024-06-vultisig-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details




The use of slot0 to obtain `sqrtPriceX96Existing` and `sqrtPriceX96` is heavily discouraged as it is easy to manipulate as it represents the current price rather a time weighted price. An attacker can therefore use flash loans to shift the slot0 by doing large swaps on Uniswap.
## Impact
UniswapV3 slot0 should not be used in `_initUniV3PoolIfNecessary` and `launch` function in the `ILOManager.sol` contract as it can be manipulated.

## Proof of Concept
slot0 is used to obtain `sqrtPriceX96Existing` and `sqrtPriceX96` as seen below.

[ILOManager.sol#L115](https://github.com/code-423n4/2024-06-vultisig/blob/main/src/ILOManager.sol#L115)

```javascript
    function _initUniV3PoolIfNecessary(PoolAddress.PoolKey memory poolKey, uint160 sqrtPriceX96) internal returns (address pool) {
        pool = IUniswapV3Factory(UNIV3_FACTORY).getPool(poolKey.token0, poolKey.token1, poolKey.fee);
        if (pool == address(0)) {
            pool = IUniswapV3Factory(UNIV3_FACTORY).createPool(poolKey.token0, poolKey.token1, poolKey.fee);
            IUniswapV3Pool(pool).initialize(sqrtPriceX96);
        } else {
-->          (uint160 sqrtPriceX96Existing, , , , , , ) = IUniswapV3Pool(pool).slot0();
            if (sqrtPriceX96Existing == 0) {
                IUniswapV3Pool(pool).initialize(sqrtPriceX96);
            } else {
                require(sqrtPriceX96Existing == sqrtPriceX96, "UV3P");
            }
        }
    }
```

[ILOManager.sol#L189](https://github.com/code-423n4/2024-06-vultisig/blob/main/src/ILOManager.sol#L189)
```javascript
function launch(address uniV3PoolAddress) external override {
        require(block.timestamp > _cachedProject[uniV3PoolAddress].launchTime, "LT");
-->     (uint160 sqrtPriceX96, , , , , , ) = IUniswapV3Pool(uniV3PoolAddress).slot0();
        require(_cachedProject[uniV3PoolAddress].initialPoolPriceX96 == sqrtPriceX96, "UV3P");
        address[] memory initializedPools = _initializedILOPools[uniV3PoolAddress];
        require(initializedPools.length > 0, "NP");
        for (uint256 i = 0; i < initializedPools.length; i++) {
            IILOPool(initializedPools[i]).launch();
        }

        emit ProjectLaunch(uniV3PoolAddress);
    }
```

References : [RealWagmi](https://solodit.xyz/issues/h-3-usage-of-slot0-is-extremely-easy-to-manipulate-sherlock-none-realwagmi-git) , [Maia DAO Ecosystem](https://solodit.xyz/issues/h-02-use-of-slot0-to-get-sqrtpricelimitx96-can-lead-to-price-manipulation-code4rena-maia-dao-ecosystem-maia-dao-ecosystem-git)

## Tools Used
Manual review.

## Recommended Mitigation Steps
The protocol should consider using the TWAP price.






## Assessed type

Uniswap
ILOPool whitelist can be bypassed by transferring nft token
Audit: 2024-06-vultisig-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
ILOPool whitelist can be easily bypassed by transferring the minted nft token, because raise amount is usually capped, if many nft tokens are minted and transferred to non-whitelisted users, raise amount can be quickly used up and some qualified whitelisted users won't be able to buy ILO as expected.

## Proof of Concept
If [_openToAll](https://github.com/code-423n4/2024-06-vultisig/blob/cb72b1e9053c02a58d874ff376359a83dc3f0742/src/base/ILOWhitelist.sol#L9C18-L9C28) is set to `false`, only the whitelisted user can buy and owns ILO nft tokens.
```
    function buy(uint256 raiseAmount, address recipient)
        external override 
        returns (
            uint256 tokenId,
            uint128 liquidityDelta
        )
    {
@>      require(_isWhitelisted(recipient), "UA");
        ...
    {
```
However, the minted ILO nft token can be transferred without restriction, a whitelisted user may be bribed to mint many nft tokens and sell/transferred to non-whitelisted users, renders the whitelist mechanism basically useless.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Override ERC721's ___beforeTokenTransfer()__ and only allow nft tokens be transferred between whitelisted users.
```
    function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal override {
        if (from != address(0) && to != address(0)) {
            require(_isWhitelisted(from) && _isWhitelisted(to), "UA");
        }
    }
```














## Assessed type

Access Control
Possible significant discrepancies in the quoted price due to hard-corded slippage Percentage in UniswapV3Oracle Contract
Audit: 2024-06-vultisig-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-06-vultisig/blob/cb72b1e9053c02a58d874ff376359a83dc3f0742/hardhat-vultisig/contracts/oracles/uniswap/UniswapV3Oracle.sol#L45


# Vulnerability details

## Proof of Concept
The `UniswapV3Oracle` contract is designed to calculate the time-weighted average price (TWAP) for the VULT/ETH trading pair over the last 30 minutes. This price calculation is used in other contracts to determine the amount of ETH required for transactions involving the VULT token. The `peek` function in this contract retrieves the TWAP price and then applies a hardcoded 5% slippage to the result. The relevant portion of the code is shown below:
```solidity
function peek(uint256 baseAmount) external view returns (uint256) {
    uint32 longestPeriod = OracleLibrary.getOldestObservationSecondsAgo(pool);
    uint32 period = PERIOD < longestPeriod ? PERIOD : longestPeriod;
    int24 tick = OracleLibrary.consult(pool, period);
    uint256 quotedWETHAmount = OracleLibrary.getQuoteAtTick(tick, BASE_AMOUNT, baseToken, WETH);
    // Apply 5% slippage
    return (quotedWETHAmount * baseAmount * 95) / 1e20; // 100 / 1e18
}
```
- The function first determines the longest available observation period using `OracleLibrary.getOldestObservationSecondsAgo(pool)`.
- It then sets the period to either the constant PERIOD (30 minutes) or the longest available period, whichever is shorter.
- The TWAP tick value over this period is fetched using `OracleLibrary.consult(pool, period)`.
- Using the tick value, the quoted WETH amount for the `BASE_AMOUNT` (`1 VULT`) is obtained with `OracleLibrary.getQuoteAtTick(tick, BASE_AMOUNT, baseToken, WETH)`.
- The function then applies a hardcoded 5% slippage to the quoted amount by multiplying it by 95% $(95 / 100)$, which adjusts the quoted WETH amount downwards to account for slippage.

## Impact
- `Lack of Flexibility`: A fixed 5% slippage does not adapt to changing market conditions. Market volatility can vary significantly, and a static slippage percentage may not provide an accurate reflection of the current market state.
- `Inaccuracy in Pricing`: If the actual market slippage is higher or lower than 5%, the calculated amount of WETH will be inaccurate. For instance, during periods of high volatility, the actual slippage might exceed 5%, resulting in the user receiving less WETH than expected.
The purpose of the slippage is to account for potential price movement between the time the TWAP is calculated and the time the transaction is executed. However, because this value is hardcoded, it cannot be adjusted to reflect real-time market conditions, which undermines its effectiveness.
## Tools Used
Manual
## Recommended Mitigation Steps
This can be mitigated with ,allowing users to define the slippage as parameters & use the amount of ETH received calculated with difference between before and after eth balances.





## Assessed type

Other
slot0 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 the contract `ILOManager` the function `launch` use `IUniswapV3Pool(uniV3PoolAddress)` to get the value of `sqrtPriceX96` relying on slot0 as the most recent data point and can be manipulated easily via MEV bots and Flashloans with sandwich attacks; which can cause the loss of funds when interacting with the Uniswap.swap function.

## Proof of Concept
```javascript
function launch(address uniV3PoolAddress) external override {
        require(block.timestamp > _cachedProject[uniV3PoolAddress].launchTime, "LT");
        (uint160 sqrtPriceX96, , , , , , ) = IUniswapV3Pool(uniV3PoolAddress).slot0();// @audit--info slot0 manipulation
        require(_cachedProject[uniV3PoolAddress].initialPoolPriceX96 == sqrtPriceX96, "UV3P");
        address[] memory initializedPools = _initializedILOPools[uniV3PoolAddress];
        require(initializedPools.length > 0, "NP");
        for (uint256 i = 0; i < initializedPools.length; i++) {
            IILOPool(initializedPools[i]).launch();
        }

        emit ProjectLaunch(uniV3PoolAddress);
    }
```

 An attacker can simply manipulate the `sqrtPriceX96` to benefit themselves

## Tools Used
Manual Review

## Recommended Mitigation Steps
 Use the TWAP function to get the value of `sqrtPriceX96`.


## Assessed type

MEV