xanoutas/solidity-security-findings huggingface dataset:
Strict require statement while launching pool
Audit: 2024-06-vultisig-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
When pool is launched it checks the sqrtPriceX96 from slot0 and project's initial sqrtPriceX96
```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();
        }

        emit ProjectLaunch(uniV3PoolAddress);
    }
```
This check will revert the transaction because sqrtPriceX96 can change because of adding or removing liquidity or swapping

## Proof of Concept
https://github.com/code-423n4/2024-06-vultisig/blob/cb72b1e9053c02a58d874ff376359a83dc3f0742/src/ILOManager.sol#L187C2-L198C6

## Tools Used
VS code

## Recommended Mitigation Steps
Create uniswap pool while launching pool or remove this check


## Assessed type

Error
User can bypass their max user cap during sale
Audit: 2024-06-vultisig-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details


The ILOPool::buy function calculates the raised amount of tokens by user by checking the ERC721 token that they own and checking the amount raised assigned to the nft. 
When a whitelist is used and every user has their max cap set, a whitelisted user can bypass their user cap by buying for the amount equal to the maxCapPerUser then transfering their nft to another 
address which they control (address they generated and they know its private key).
After transferring the nft user can call the function again, 
the contract would check nft they own and then, because user address does not own any nfts assume that user did not buy anything and allow them to buy more positions. 
User can repeat that process until the sale reaches the hard cap.
After their purchases they can transfer their nft position back to themselves and claim them after the pool launches.
Especially during scenario where the sale is whitelisted, user could buy a large portion of liquidity positions thus owning the amount of liquidity 
that was not intended to be sold one the user by the contract.
In scenario where there is e.g 10 whitelisted users, hard Cap - 10 tokens and maxCapPerUser 1 token, malicious user could buy for all 10 tokens thus other users cannot buy.


## Proof of Concept
The next test added to the ILOPool.t.sol file could show a scenario when the user bypasses their maxCapPerUser:
```
 /// cap set to 60000 ether
    function testExceedMaxPerCap() external {
        //Setup a whitelist where INVESTOR address is whitelisted
        vm.prank(PROJECT_OWNER);
        IILOWhitelist(iloPool).batchWhitelist(_getListAddressFromAddress(INVESTOR));

        //Investor approves pool so they can buy in ILOPool
        _writeTokenBalance(USDC, INVESTOR, 1000000000 ether);
        vm.prank(INVESTOR);
        IERC20(USDC).approve(iloPool, 1000000000 ether);

        //We change time to a time when the sale is active
        vm.warp(SALE_START+1);
        vm.prank(INVESTOR); 
        // Investor buys 60000 which is a max amount per user and gets minted an nft 
        IILOPool(iloPool).buy(60000 ether, INVESTOR);
        address ownerOfToken_1 = ERC721(iloPool).ownerOf(1);
        assertEq(ownerOfToken_1, INVESTOR);

        // When the whitelisted investor tries to buy more, contract declines because they would exceed the cap
        vm.prank(INVESTOR);
        vm.expectRevert(bytes("UC"));
        IILOPool(iloPool).buy(1 ether, INVESTOR);

        // Investor transfers the nft to their another address that they control
        vm.prank(INVESTOR);
        ERC721(iloPool).transferFrom(INVESTOR, DUMMY_ADDRESS, 1);
        
        // Now they can successfully buy more tokens exceeding their cap 
        vm.prank(INVESTOR); 
        IILOPool(iloPool).buy(40000 ether, INVESTOR);
        // Investor can do that until the hard
Slot0 is vulnerable to manipulations
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
The slot0 is the most recent data point and is therefore extremely easy to manipulate.

## Proof of Concept
Here slot0 function returns various parameters of the pool, including the sqrtPriceX96 value. A big swap using a flash loan can push the liquidity to one side only.

https://docs.uniswap.org/contracts/v3/reference/core/interfaces/pool/IUniswapV3PoolState#slot0

## Tools Used
Foundry

## Recommended Mitigation Steps
Don't use slot0. Is better to use TWAP Oracle instead.


## Assessed type

Uniswap
ILOPool.sol:buy() Method Can Be Used To Manipulate ILO Claiming Period by Minting Multiple NFTs
Audit: 2024-06-vultisig-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Detailed description of the impact of this finding.
The buy function in ILOPool.sol is designed to limit users to minting just one NFT each. However, a critical vulnerability allows attackers to bypass this restriction and claim a majority of the rewards.
## Proof of Concept
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
The vulnerability lies in the following check : 
```
        // 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);
        }

```
The contract aims to ensure that investors don't hold any NFTs before minting during the purchases. It achieves this by calling the balanceOf function of the ERC721 standard. However, there's a vulnerability. If an investor sends his NFT to another address, the balanceOf check on the recipient's address will return 0. This allows a malicious actor to bypass the restriction.

The exploit involves the attacker creating two Externally Owned Accounts (EOAs) and whitelisting both of them. During each purchase, the attacker would first buy an NFT with account 1 and then transfer it to account 2. By exploiting the balanceOf check, the attacker can claim multiple NFTs during the claiming period, unlike legitimate investors.

The following code simulates this attack scenario.
```
// SPDX-License-Identifier: MIT 

pragma solidity =0.7.6;
pragma abicoder v2;

import "./IntegrationTestBase.sol";
import '../src/interfaces/IILOPool.sol';
import '../src/interfaces/IILOWhitelist.sol';
import '../src/interfaces/IILOVest.sol';
import  '@openzeppelin/contracts/token/ERC721/IERC721.sol';

contract ILOPoolTest is IntegrationTestBase {
    address iloPool;
    function setUp() external {
        _setupBase();
        iloPool = _initPool(PROJECT_OWNER, _getInitPoolParams());
    }

    function testAttackerMintsMultipleNFTs(address attacker1,address attacker2) external{
        uint256 tokenId;
        //assume that attacker owns 2 address 
        attacker1 = address(0x1);
        attacker2 = address(0x2);
        //save attacker's usdc balance
        uint256 beforeAttack = IERC20(USDC).balanceOf(attacker2);
        //attacker first mints saletoken nft with attacker2 eoa
        _prepareBuyFor(attacker2);
        (tokenId,) = _buyFor(attacker2, SALE_START + 1, 0.1 ether);
        //attacker mints more nft than he supposed to mint as transferring minted nfts to attacker2
        for(uint64 i=2;i<=7;i++){
            _prepareBuyFor(attacker1);
`ILOPool::claim` should have slippage protection
Audit: 2024-06-vultisig-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
`ILOPool::buy` allows whitelisted recipients to invest in a project and mints an NFT of their position corresponding to liquidity amount for the amount of `raise tokens` they transferred.

`ILOPool::claim` allows users to claim tokens associated with their position ID, and returns the amounts of token0 and token1 that were collected for the position represented by the position (token) ID.

This is done by calling external `burn()` and `collect()` functions of the `Uniswap v3 pool`. The problem is the amount they receive may change prior to execution (i.e, while it's in the mempool), which can cause users to receive much less amount than they are owed.

## Proof of Concept
[ILOPool.sol#L205)](https://github.com/code-423n4/2024-06-vultisig/blob/main/src/ILOPool.sol#L205)
```javascript
    // get amount of token0 and token1 that pool will return for us
    (amount0, amount1) = pool.burn(TICK_LOWER, TICK_UPPER, liquidity2Claim);
```

The external `Uniswap v3 pool` is called with the amount of liquidity the caller is eligible to claim, and this amount is burned. The caller is returned the amount of tokens received from the burn.

When the external burn call is executed, to calculate the amount of tokens the caller will receive, the following is executed from the `Uniswap V3 pool`

[UniswapV3Pool.sol#L336-L359](https://github.com/Uniswap/v3-core/blob/main/contracts/UniswapV3Pool.sol#L336-L359)
```javascript
    } else if (_slot0.tick < params.tickUpper) {
        // current tick is inside the passed range
        uint128 liquidityBefore = liquidity; // SLOAD for gas optimization

        // write an oracle entry
        (slot0.observationIndex, slot0.observationCardinality) = observations.write(
            _slot0.observationIndex,
            _blockTimestamp(),
            _slot0.tick,
            liquidityBefore,
            _slot0.observationCardinality,
            _slot0.observationCardinalityNext
        );

        amount0 = SqrtPriceMath.getAmount0Delta(
            _slot0.sqrtPriceX96,
            TickMath.getSqrtRatioAtTick(params.tickUpper),
            params.liquidityDelta
        );
        amount1 = SqrtPriceMath.getAmount1Delta(
            TickMath.getSqrtRatioAtTick(params.tickLower),
            _slot0.sqrtPriceX96,
            params.liquidityDelta
        );
```

These values can change prior to execution, another user may also front-run to claim before, potentially causing the caller to receive less tokens.

## Tools Used
Manual review

## Recommended Mitigation Steps
Incorporate slippage protection in `ILOPool::claim`


## Assessed type

Context
User can bypass `maxCapPerUser`.
Audit: 2024-06-vultisig-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Vulnerability details

In `buy` function there is a restriction that single user can not buy more than `maxCapPerUser`.

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

However this require statement can be easily bypassed by transfering current NFT to other address controlled by a user. Then he can buy tokens (will also receive new NFT) and will bypass this check. After purchase he can transfer back his first NFT. User can do it as many times as he wishes using only 2 of his addresses.

```javascript
    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;
```

## Impact

Protocol requirements can be bypassed by NFT transfer.

## Recommended Mitigation Steps

Make NFT non transferable for sale time. It will prevent users from transfering NFT to other addresses and buying more tokens bypassing max user cap check.


## Assessed type

Invalid Validation
Malicious ILO Participants Can Use Whitelisted Address To Buy Multiple NFT Positions For Non-Whitelisted Addresses
Audit: 2024-06-vultisig-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
A malicious whitelisted address can `buy` and transfer out NFT positions to non-whitelisted addresses.

`ILOPool::buy` has a check to see if the recipient address is whitelisted (or open-to-all) and if the address already has an NFT position. These checks ensure that only Whitelisted addresses can buy an NFT position (when not open-to-all) and if the address already has an NFT position then the position's `raiseAmount` and `liquidity` will be increased rather than minting a new NFT position for the addresses (i.e to avoid double or multiple minting of positions for an address).


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

```javascript
        require(_isWhitelisted(recipient), "UA");
```
https://github.com/code-423n4/2024-06-vultisig/blob/cb72b1e9053c02a58d874ff376359a83dc3f0742/src/ILOPool.sol#L143

```javascript
       if (balanceOf(recipient) == 0) {..}
```
However, a malicious whitelisted address can still buy multiple NFT positions for non-whitelisted addresses by transferring each NFT position bought at the time and bypassing this check `if (balanceOf(recipient) == 0)` to buy another position. 

This will impact the `ILO sales` whereby only a certain number of whitelisted addresses are allowed to participate in the sales (e.g. 1000 whitelists) for a particular period before opening to all. The malicious whitelisted address can then buy multiple NFT positions for non-whitelisted addresses thereby increasing the sales, and impacting the `totalRaised` (to near the capped amount of `raise tokens` allowed) which reduces NFT positions that could be bought and also defeats the whole idea of whitelist.

## Proof of Concept
Run `forge test --match-test test_multiple_NFT_positions -vvvv` in `ILOPool.t.sol`.

```javascript
    function test_multiple_NFT_positions() external {
        //for a whitelisted address
        _prepareBuy();

        //First NFT position (by malicious whitelisted address)
        (uint256 tokenId1, uint128 liquidity1) = _buy(SALE_START+1, 0.1 ether);

        //Transfer NFT position (to non-whitelisted address)
        vm.prank(INVESTOR);
        IERC721(iloPool).transferFrom(INVESTOR, DUMMY_ADDRESS, tokenId1);

        //Second NFT position (by malicious whitelisted address)
        (uint256 tokenId2, uint128 liquidity2) = _buy(SALE_START+1, 0.1 ether);

        //view NFT positions of both whitelisted and non-whitelisted
        IILOPool(iloPool).positions(tokenId1);
        IILOPool(iloPool).positions(tokenId2);

    }
```

## Tools Used
Manual and Foundry

## Recommended Mitigation Steps
* [short term] Make NFT positions non-transferable during the whitelist sale perio
User can bypass `maxCapPerUser` by transfering tokens to other accounts
Audit: 2024-06-vultisig-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
The current implementation allows users to bypass the `maxCapPerUser` restriction by transferring their NFT position to another account. 

```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) {    //@check what if transfer?
            _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");
```
This loophole enables users to repeatedly invest up to the maxCapPerUser limit by continually transferring their position and re-purchasing tokens. This behavior undermines the purpose of maxCapPerUser, which is to limit the total investment a single user can make. Additionally, it violates the restriction intended to ensure that each user holds only a single NFT position.


## Proof of Concept
```
    function testBuyTooMuch() external {
        address frnd = address(0x1447);
        _prepareBuy();

        _buyFor(INVESTOR, SALE_START+1, 60000 ether);

        vm.startPrank(INVESTOR);
        IILOPool(iloPool).approve(frnd, 1);
        IILOPool(iloPool).transferFrom(INVESTOR, frnd, 1);
        vm.stopPrank();

        _buyFor(INVESTOR, SALE_START+1, 40000 ether);
    }

```

## Tools Used
Foundry, Manual Review

## Recommended Mitigation Steps
Implement a separate mechanism to keep track of user's tokens minted instead of depending on `balanceOf`.




## Assessed type

Other
Whitelisted Users Can Bypass `maxCapPerUser` in `ILOManager::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#L141-L152


# Vulnerability details

## Issue Description

The `ILOPool::buy` function allows whitelisted users to buy pool liquidity shares before liquidity is launched to UniV3. To buy liquidity, they must pay a certain amount of RAISE tokens. Each whitelisted user has a maximum amount of RAISE tokens they can use, represented by `maxCapPerUser`. This limit is checked inside the function, as shown below:

```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 has 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;

    ...
}
```

The issue that arises is that the cap is tracked on a per token (ERC721 position token) basis and not on the whitelisted user address (represented by `recipient` above). The following scenario can occur:

* Suppose RAISE token is ETH and `maxCapPerUser = 10 ETH`.
* Bob is a whitelisted user who calls the `ILOPool::buy` function to get liquidity by sending 10 ETH.
* Bob receives an NFT representing his position `tokenId = 5`, and as he already reached the buy cap, he shouldn't be able to buy again.
* Bob transfers his newly minted position NFT to another address (another wallet he owns).
* Bob calls `ILOPool::buy` function again. He is able to pass the check `balanceOf(recipient) == 0` as he doesn't hold any NFT in his current address (it was transferred in the step before), thus Bob gets minted another ERC721 position `tokenId = 6`.
* Because Bob with his new NFT, buys liquidity again by sending 10 ETH.
* Bob now owns two position NFTs the one with `tokenId = 6` in his first wallet and the second `tokenId = 5` that he transferred to the other wallet.
* Bob was able to bypass the `maxCapPerUser = 10 ETH` as he bought 20 ETH worth of liquidity, and he can repeat this process as many times as he wants.

In the above scenario, Bob, a whitelisted user, was able to bypass the maximum cap set for each user by repeatedly transferring the NFT he get minted to another address (in o
A `DoS` attack on the launch.
Audit: 2024-06-vultisig-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

Attackers can disrupt the launch by manipulating the price of the Uniswap V3 pool.

## Proof of Concept

As shown in the code at `L190`, the launch requires the price of the Uniswap V3 pool to remain unchanged. However, an attacker could exploit this by adding a small amount of liquidity to the Uniswap V3 pool and then executing a swap, which would change the pool price. This manipulation could potentially trigger the reversal of the launch.

```solidity
    function launch(address uniV3PoolAddress) external override {
        require(block.timestamp > _cachedProject[uniV3PoolAddress].launchTime, "LT");
        (uint160 sqrtPriceX96, , , , , , ) = IUniswapV3Pool(uniV3PoolAddress).slot0();
190     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);
    }
```

## Tools Used

Manual review

## Recommended Mitigation Steps

A mechanism should be implemented to prevent manipulation of the Uniswap V3 pool price before the launch.


## Assessed type

DoS
High hard-coded slippage
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#L38-L46


# Vulnerability details

## Impact
Tokens purchase through whitelist could be more expensive than purchase through Uniswap (or other DEX)

## Proof of Concept
5% slippage is considered to be a high amount for token conversions. Most non-exotic pairs on Uniswap can be converted using 0.3% to 1% slippage value.

For this reason, users could prefer to use Uniswap to buy tokens instead of using the whitelist to be allocated <x> tokens.

https://github.com/code-423n4/2024-06-vultisig/blob/cb72b1e9053c02a58d874ff376359a83dc3f0742/hardhat-vultisig/contracts/Whitelist.sol#L230

## Tools Used
NA

## Recommended Mitigation Steps
I would recommend removing slippage value or reducing it to a value between 0.3% to 1% to ensure buying tokens through whitelist is relatively cheaper (/competitive) than buying tokens off of Uniswap (or any other DEX)





## Assessed type

Uniswap