code-423n4 / 2024-06-vultisig-validation

2 stars 0 forks source link

A malicious user ca grief All Pool launches of any Project due to the strict check of `sqrtPriceX96` in `ILOManager::launch() #685

Open c4-bot-7 opened 4 months ago

c4-bot-7 commented 4 months ago

Lines of code

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

Vulnerability details

Impact

a malicious user can grief Pool creation permanently corrupting the project launch

Proof of Concept

the problem starts in the following function

File: ILOManager.sol
57:     function initProject(InitProjectParams calldata params) external override afterInitialize() returns(address uniV3PoolAddress) {
58:         uint64 refundDeadline = params.launchTime + DEFAULT_DEADLINE_OFFSET;
59: 
60:         PoolAddress.PoolKey memory poolKey = PoolAddress.getPoolKey(params.saleToken, params.raiseToken, params.fee);
61:         uniV3PoolAddress = _initUniV3PoolIfNecessary(poolKey, params.initialPoolPriceX96);
62:         
63:         _cacheProject(uniV3PoolAddress, params.saleToken, params.raiseToken, params.fee, params.initialPoolPriceX96, params.launchTime, refundDeadline);
64:         emit ProjectCreated(uniV3PoolAddress, _cachedProject[uniV3PoolAddress]);
65:     }

which as we see in Line #61 calls _initUniV3PoolIfNecessary passing initialPoolPriceX96 as parameter here

File: ILOManager.sol
109:     function _initUniV3PoolIfNecessary(PoolAddress.PoolKey memory poolKey, uint160 sqrtPriceX96) internal returns (address pool) {
110:         pool = IUniswapV3Factory(UNIV3_FACTORY).getPool(poolKey.token0, poolKey.token1, poolKey.fee);
111:         if (pool == address(0)) {
112:             pool = IUniswapV3Factory(UNIV3_FACTORY).createPool(poolKey.token0, poolKey.token1, poolKey.fee);
113:             IUniswapV3Pool(pool).initialize(sqrtPriceX96);
114:         } else {
115:             (uint160 sqrtPriceX96Existing, , , , , , ) = IUniswapV3Pool(pool).slot0();
116:             if (sqrtPriceX96Existing == 0) {
117:                 IUniswapV3Pool(pool).initialize(sqrtPriceX96);
118:             } else {
119:                 require(sqrtPriceX96Existing == sqrtPriceX96, "UV3P");
120:             }
121:         }
122:     }

in Lines #115 to #117 we initialize the pool with the preferred Price but the problem here arises that non Empty UniSwap Pools are still open for people to use and swap "meaning that its user controlled" POC will be provided whenever a malicious user manipulate the price of the uniswap pool all ILOPool launched will be useless due to the following

File: ILOManager.sol
187:     function launch(address uniV3PoolAddress) external override {
188:         require(block.timestamp > _cachedProject[uniV3PoolAddress].launchTime, "LT");
189:         (uint160 sqrtPriceX96, , , , , , ) = IUniswapV3Pool(uniV3PoolAddress).slot0();
190:         require(_cachedProject[uniV3PoolAddress].initialPoolPriceX96 == sqrtPriceX96, "UV3P");
191:         address[] memory initializedPools = _initializedILOPools[uniV3PoolAddress];
192:         require(initializedPools.length > 0, "NP");
193:         for (uint256 i = 0; i < initializedPools.length; i++) {
194:             IILOPool(initializedPools[i]).launch();
195:         }
196: 
197:         emit ProjectLaunch(uniV3PoolAddress);
198:     }

as we see in line #189 we retrieve the Price from slot0 then strictly check the equivalence with project configuration at Line #192 which will lead to all Pools not being initialized

foundry POC that shows that empty Pools are actually manipulatable

contract UniswapV3PoolSwapTest is Test {
    UniswapV3Pool public pool;
    TestERC20 public tokenA;
    TestERC20 public tokenB;

    uint24 public constant FEE_AMOUNT = 3000; // 0.3%
    int24 public constant TICK_SPACING = 60;
    int24 public constant INITIAL_TICK = 0;

    function setUp() public {
        tokenA = new TestERC20("Token A", "TKNA", 18);
        tokenB = new TestERC20("Token B", "TKNB", 18);

        pool = new UniswapV3Pool(
            address(tokenA),
            address(tokenB),
            FEE_AMOUNT,
            TICK_SPACING
        );

        // Initialize the pool with a specific sqrtPriceX96
        int24 initialTick = INITIAL_TICK;
        uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(initialTick);
        pool.initialize(sqrtPriceX96);
    }

    function testSwapWithoutLiquidity() public {
        uint256 amountIn = 10 * 1e18; // 10 Token A

        // Get the initial slot0 values
        (uint160 initialSqrtPriceX96, int24 initialTick, , , , , ) = pool.slot0();

        // Swap Token A for Token B
        vm.startPrank(address(this));
        tokenA.mint(address(this), amountIn);
        tokenA.approve(address(pool), amountIn);
        pool.swap(address(this), false, amountIn, 0, TickMath.MAX_SQRT_RATIO);
        vm.stopPrank();

        // Get the updated slot0 values
        (uint160 newSqrtPriceX96, int24 newTick, , , , , ) = pool.slot0();

        // Check if the swap affected the price (sqrtPriceX96)
        assertnEq(newSqrtPriceX96, initialSqrtPriceX96);
        assertnEq(newTick, initialTick);
    }
}

Tools Used

manual review and foundry

Recommended Mitigation Steps

consider using TWAP oracle

Assessed type

Uniswap