1

I'm concerning about why I'm getting an error trying to add liquidity using the addLiquidityETH to a UniswapV2Pair during construction. It seems that it reverts when the router calls the IUniswapV2Pair(pair).mint(to); method.

If I, instead, call the addLiquidityETH in a second transaction after token deployment it works.

The following calls the _addLiquidity method in constructor but the transaction fails (trx)

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol";
import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol";

contract SwappableLiquidityInDeploy is ERC20 {
    IUniswapV2Router02 private immutable _uniswapV2Router;
    address private immutable _uniswapV2Pair;
    
    constructor(address uniswapV2Router_) ERC20("Swappable", "SWAP") payable {
        uint256 initialBalance = 100000000 * 10 ** decimals();
    
        _mint(address(this), initialBalance);

        _uniswapV2Router = IUniswapV2Router02(uniswapV2Router_);

        _uniswapV2Pair = IUniswapV2Factory(_uniswapV2Router.factory()).createPair(
            address(this),
            _uniswapV2Router.WETH()
        );

        _addLiquidity(msg.value, initialBalance);
    }

    function _addLiquidity(uint256 ethAmount, uint256 tokenAmount) internal {
        _approve(address(this), address(_uniswapV2Router), tokenAmount);

        _uniswapV2Router.addLiquidityETH{value: ethAmount}(
            address(this),
            tokenAmount,
            0,
            0,
            address(this),
            block.timestamp
        );
    }
}

This one, instead, creates the contract first (trx) and then calls the addLiquidityMock method to add liquidity without errors (trx).

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol";
import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol";

contract SwappableLiquidityInFunction is ERC20 {
    IUniswapV2Router02 private immutable _uniswapV2Router;
    address private immutable _uniswapV2Pair;
    
    constructor(address uniswapV2Router_) ERC20("Swappable", "SWAP") payable {
        uint256 initialBalance = 100000000 * 10 ** decimals();
    
        _mint(address(this), initialBalance);

        _uniswapV2Router = IUniswapV2Router02(uniswapV2Router_);

        _uniswapV2Pair = IUniswapV2Factory(_uniswapV2Router.factory()).createPair(
            address(this),
            _uniswapV2Router.WETH()
        );
    }

    function addLiquidityMock() public payable {
        _addLiquidity(msg.value, totalSupply());
    }

    function _addLiquidity(uint256 ethAmount, uint256 tokenAmount) internal {
        _approve(address(this), address(_uniswapV2Router), tokenAmount);

        _uniswapV2Router.addLiquidityETH{value: ethAmount}(
            address(this),
            tokenAmount,
            0,
            0,
            address(this),
            block.timestamp
        );
    }
}

I tried on Goerli and Mainnet, other than locally using the precompiled Uniswap build, receiving Error: Transaction reverted without a reason string

1 Answers1

1

Your constructor calls Uniswap Router's addLiquidityETH().

This function tries to call your contract's transferFrom() balanceOf() through a series of function calls starting here.

But since the constructor of your token contract hasn't finished executing, its bytecode has not been stored yet, and the other functions are not yet available.

In other words, you cannot call functions of a contract that is not yet fully deployed.

Most straightfoward solution is to split the deployment and liquidity providing into 2 steps. It's also often good idea to implement validations that the liquidity providing function can be called only by the deployer, and only once.

Petr Hejda
  • 40,554
  • 8
  • 72
  • 100
  • Following the [stack trace](https://goerli.etherscan.io/vmtrace?txhash=0x89a44ae594e0fba829594a1a954480374129551d0280d90f05d2ef7b98230d5c&type=parity) it seemed that the router can use token contract before the end of the execution. In Action[10] router calls the transferFrom on contract and it reverts on Action[13] that should be the pair.mint. Anyway I thought the problem was the one highlighted in your answer, I was trying to figure out if there was a workaround. And yes the add liquidity method is only a mock without ownership check. – vittominacori May 16 '23 at 14:14
  • @vittominacori The successful `transfer` and `transferFrom` calls are on the `WETH` token - not on the `SWAP` token. It might be more easily visible using another tool, for example [this one](https://dashboard.tenderly.co/tx/goerli/0x89a44ae594e0fba829594a1a954480374129551d0280d90f05d2ef7b98230d5c). – Petr Hejda May 16 '23 at 14:55
  • Also there seems that it is the pair.mint failing while the `UniswapV2Router02.safeTransferFrom` should transfer SWAP tokens into the pair correctly and then WETH. Later the mint on pair reverts. Am I wrong? – vittominacori May 16 '23 at 15:13
  • The `mint()` function tries to call your token's `balanceOf()`, which fails as the function is not yet available. Source: https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2Pair.sol#L112 ... My original answer assumed that `transferFrom()` is called first, but now it seems that `balanceOf()` is called first, which actually makes more sense in this context. I updated the answer. – Petr Hejda May 16 '23 at 15:30
  • Still cannot see why the `safeTransferFrom` using `SWAP` token doesn't fail before calling `mint` and `balanceOf` fails. Source: https://github.com/Uniswap/v2-periphery/blob/master/contracts/UniswapV2Router02.sol#L94 – vittominacori May 16 '23 at 15:39