Solidity – How to Resolve Compiler Version Conflicts in Foundry Test Contracts

compilationforgefoundrysolidityversion-pragma

I'm working on a Foundry project, and I'm encountering issues with compiler versions when trying to deploy Uniswap V3 core and periphery contracts in a test contract(i.e. UniswapTest shown below). The test contract uses solidity 0.8, but v3-core and v3-periphery use solidity 0.7.6:

// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.0;
pragma abicoder v2;

import { StdCheats } from "forge-std/StdCheats.sol";

import { IUniswapV3Factory } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol";
import { UniswapV3Factory } from "@uniswap/v3-core/contracts/UniswapV3Factory.sol";
import { INonfungiblePositionManager } from "@uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol";
import { NonfungiblePositionManager } from "@uniswap/v3-periphery/contracts/NonfungiblePositionManager.sol";

contract UniswapTest is StdCheats {

    // Uniswap V3 contracts
    IUniswapV3Factory internal uniswapV3Factory;
    INonfungiblePositionManager internal nonfungiblePositionManager;

    function setUp() public virtual {
        uniswapV3Factory = new UniswapV3Factory();
        nonfungiblePositionManager = new NonfungiblePositionManager(address(uniswapV3Factory), wethAddress);
    }

    function test_Uniswap() external {
      // ... test uniswap
    }
}

Attempting to forge compile results the following truncated compilation error:

Discovered incompatible solidity versions in following
: test/Uniswap.t.sol (>=0.8.0) imports:
    lib/v3-core/contracts/UniswapV3Factory.sol (=0.7.6)
    it continues to list all other Uniswap.t.sol imports some of which are compatible with >=0.8.0 and some which are not as is the case with the UniswapV3Factory import above

Additionally if pragma solidity >=0.8.0 <0.9.0; is used instead of pragma solidity >=0.8.0; for the Uniswap.t.sol file this results in the following compiler error:

Encountered invalid solc version in test/U.t.sol: Failed to parse solidity version >=0.8.0 <0.9.0: expected comma after patch version number, found '<'

How can I deploy contracts that require a solidity version that is incompatible with the test contract's solidity version?

This question is somewhat related to this one, however it does not have a satisfactory answer.

Best Answer

Instead of trying to import contracts that are incompatible with your test contract's version and then attempting to deploy those contracts using the new keyword, you can instead use the StdCheats#deployCode() function. If you want to deploy contract/s from an external library this way you'll have to first import those contract/s into one of your project files so that foundry compiles and generates the artifacts for these contracts in the output directory(i.e. the ./out folder by default). For example you can add these imports in a ./test/mocks/Uniswap.sol file and after compiling should observe the artifacts for these contracts in the ./out folder:

// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.7.0;

import { UniswapV3Factory } from "@uniswap/v3-core/contracts/UniswapV3Factory.sol";
import { NonfungiblePositionManager } from "@uniswap/v3-periphery/contracts/NonfungiblePositionManager.sol";

Now in your test file you can deploy these contracts as follows:

contract UniswapTest is StdCheats {

    // Uniswap V3 contracts
    IUniswapV3Factory internal uniswapV3Factory;
    INonfungiblePositionManager internal nonfungiblePositionManager;

    function setUp() public virtual {
      IUniswapV3Factory uniswapV3Factory = IUniswapV3Factory(deployCode("UniswapV3Factory.sol"));
      INonfungiblePositionManager = INonfungiblePositionManager(deployCode("NonfungiblePositionManager.sol", abi.encode(address(uniswapV3Factory), wethAddress)));
    }

    function test_Uniswap() external {
      // ... test uniswap
    }
}

However you may not want to deploy bytecode generated by foundry(given the configured compiler & optimizer settings) but instead deploy a specific bytecode for a contract for a reason such as the following:

In the context of Uniswap the v3-periphery repo has a PoolAddress library with a POOL_INIT_CODE_HASH constant which is the keccak256 hash of the UniswapV3Pool contract creation bytecode defined in v3-core, and the PoolAddress library is a dependency of many periphery contracts including NonfungiblePositionManager, so in order to have the periphery contracts function in your local foundry environment you'll want to deploy the exact UniswapV3Factory bytecode(which contains the UniswapV3Pool creation bytecode within it). The factory bytecode can be generated by running hardhat compile on the Uniswap/v3-core repo and should be visible in the ./artifacts/contracts/UniswapV3Factory.sol/UniswapV3Factory.json file. You can then copy and paste the bytecode in to a Precompiles contract defined as follows:

// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.7.0;

import { IUniswapV3Factory } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol";

contract Precompiles {

    bytes private constant UNISWAP_V3_FACTORY_0_7_BYTE_CODE =
        hex"60a0604052348..."; // bytecode is too long to show in this message

    // taken from https://ethereum.stackexchange.com/q/132029/86303
    function deployCode(bytes memory bytecode) internal returns (address payable addr) {
        assembly {
            addr := create(0, add(bytecode, 0x20), mload(bytecode))

            if iszero(extcodesize(addr)) {
                returndatacopy(0, 0, returndatasize())
                revert(0, returndatasize())
            }
        }
    }

    function deployUniswapV3Factory() public returns (IUniswapV3Factory uniswapV3Factory) {
        bytes memory creationBytecode = UNISWAP_V3_FACTORY_0_7_BYTE_CODE;
        uniswapV3Factory = IUniswapV3Factory(deployCode(creationBytecode));
    }
}

You can then deploy the UniswapV3Factory(with the exact bytecode as Uniswap's V3 Factory deployments) in your test contract as follows:

contract UniswapTest is StdCheats {

    // Uniswap V3 contracts
    IUniswapV3Factory internal uniswapV3Factory;
    INonfungiblePositionManager internal nonfungiblePositionManager;

    Precompiles internal precompiles;

    function setUp() public virtual {
        precompiles = new Precompiles();
        uniswapV3Factory = precompiles.deployUniswapV3Factory();

        nonfungiblePositionManager = INonfungiblePositionManager(deployCode("NonfungiblePositionManager.sol", abi.encode(address(uniswapV3Factory), wethAddress, address(nonfungibleTokenPositionDescriptor))));
        swapRouter = ISwapRouter(deployCode("SwapRouter.sol", abi.encode(address(uniswapV3Factory), wethAddress)));
    }

    function test_Uniswap() external {
        // test uniswap...
    }
}

NB:

  • You may have noticed that I've ignored deploying the NonfungibleTokenPositionDescriptor contract which the NonfungiblePositionManager contract requires in it's constructor parameters. This is for simplicity of the example(since it's a minor dependency), but also the NonfungibleTokenPositionDescriptor contract has to be linked to the NFTDescriptor library and there's currently no way to programmatically(i.e. using solidity) link a library to a contract using StdCheats#deployCode() in foundry.
  • If you're trying to import an interface in your test contract that has an incompatible version you can simply copy paste the interface file into your project and change the pragma version directive and use this interface instead.