← Back to list

NFTPositionManager Calling to Non-Contract Address: Solving Uniswap V3’s Most Frustrating Error

The Problem That Stumps Everyone

Amlan Roy · 2026-01-27 06:31 · 0 claps · 5.3 min read
#uniswap-v3 #uniswap-v2 #nft #nft-option #trading
Open on Medium ↗
Wiki topics: CRY · Crypto & Web3

NFTPositionManager Calling to Non-Contract Address: Solving Uniswap V3’s Most Frustrating Error

The Problem That Stumps Everyone

If you’ve tried deploying Uniswap V3 to a local blockchain, testnet, or L2, you’ve probably encountered this cryptic error:

call to non-contract address 0x77BEa9Ae8EC7d3dd1b0cF167fd2d019a771e7223

You know the pool exists. You can see it on-chain. But when you try to interact with it through the NonfungiblePositionManager or Router, everything fails. What's going on?

Welcome to the POOL_INIT_CODE_HASH mismatch problem — one of the most common (and frustrating) issues when deploying Uniswap V3 from source.

What You’ll Learn

In this guide, I’ll walk you through:

  • Why this error happens (understanding CREATE2 and init code hashes)
  • How to calculate the correct hash for your deployment
  • Step-by-step instructions to fix the issue
  • Best practices for Uniswap V3 deployments

Understanding CREATE2 and Pool Address Computation

Uniswap V3 uses Ethereum’s CREATE2 opcode to deploy pools at deterministic addresses. This means the pool address can be calculated before deployment using this formula:

pool_address = keccak256(
    0xff,
    factory_address,
    keccak256(token0, token1, fee),   // salt
    POOL_INIT_CODE_HASH               // hash of pool bytecode
)

This deterministic addressing is powerful — it allows the periphery contracts (Position Manager, Router, Quoter) to calculate where a pool should be without making external calls.

The Heart of the Problem

Here’s where things go wrong. The periphery contracts use PoolAddress.sol to compute pool addresses:

// lib/v3-periphery/contracts/libraries/PoolAddress.sol
library PoolAddress {
    bytes32 internal constant POOL_INIT_CODE_HASH = 
        0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54;

    function computeAddress(
        address factory,
        PoolKey memory key
    ) internal pure returns (address pool) {
        pool = address(
            uint160(
                uint256(
                    keccak256(
                        abi.encodePacked(
                            hex'ff',
                            factory,
                            keccak256(abi.encode(key.token0, key.token1, key.fee)),
                            POOL_INIT_CODE_HASH  // ← This is hardcoded!
                        )
                    )
                )
            )
        );
    }
}

That POOL_INIT_CODE_HASH is hardcoded from Uniswap's official Ethereum mainnet deployment. When you compile the contracts yourself with:

  • Different Solidity compiler versions
  • Different optimization settings
  • Different compilation environments

You get different bytecode, which means a different init code hash, which means different pool addresses.

The NonfungiblePositionManager calculates one address (using the wrong hash), but the Factory deploys the pool to a different address (using the actual bytecode). Result? "Call to non-contract address."

The Solution: A Four-Step Process

Step 1: Calculate Your Init Code Hash

First, create a Foundry script to compute the hash from your compiled bytecode:

// script/GetPoolInitCodeHash.s.sol
pragma solidity ^0.7.6;
import "forge-std/Script.sol";
import "@uniswap/v3-core/contracts/UniswapV3Pool.sol";
contract GetPoolInitCodeHash is Script {
    function run() public view {
        bytes32 initCodeHash = keccak256(type(UniswapV3Pool).creationCode);
        console.log("POOL_INIT_CODE_HASH:");
        console.logBytes32(initCodeHash);
    }
}

Run it:

forge script script/GetPoolInitCodeHash.s.sol:GetPoolInitCodeHash

You’ll get output like:

POOL_INIT_CODE_HASH:
0x1100a600ace17973c8c8ccf7d2f8897b43d8a4eba420a7b5d1477ea1649cbc10

Important: This hash is specific to your compilation. Don’t copy hashes from other tutorials, they won’t work!

Step 2: Update PoolAddress.sol

Navigate to lib/v3-periphery/contracts/libraries/PoolAddress.sol and update the constant:

library PoolAddress {
-   bytes32 internal constant POOL_INIT_CODE_HASH = 
-       0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54;
+   bytes32 internal constant POOL_INIT_CODE_HASH = 
+       0x1100a600ace17973c8c8ccf7d2f8897b43d8a4eba420a7b5d1477ea1649cbc10;

Step 3: Redeploy All Periphery Contracts

This is crucial , you must redeploy all contracts that depend on PoolAddress.sol:

  • NonfungiblePositionManager
  • SwapRouter
  • SwapRouter02 (if using)
  • Quoter
  • QuoterV2 (if using)

Create a comprehensive deployment script:

// script/DeployUniswapV3.s.sol
pragma solidity ^0.7.6;
pragma abicoder v2;
import "forge-std/Script.sol";
import "@uniswap/v3-core/contracts/UniswapV3Factory.sol";
import "@uniswap/v3-periphery/contracts/NonfungiblePositionManager.sol";
import "@uniswap/v3-periphery/contracts/SwapRouter.sol";
import "./WETH9.sol";
contract DeployUniswapV3 is Script {
    function run() external {
        vm.startBroadcast();
        // Deploy core contracts
        UniswapV3Factory factory = new UniswapV3Factory();
        WETH9 weth = new WETH9();
        // Deploy periphery contracts
        NonfungiblePositionManager positionManager = 
            new NonfungiblePositionManager(
                address(factory),
                address(weth),
                address(0) // tokenDescriptor - can be address(0) for basic testing
            );
        SwapRouter router = new SwapRouter(
            address(factory),
            address(weth)
        );
        console.log("Factory:", address(factory));
        console.log("WETH:", address(weth));
        console.log("Position Manager:", address(positionManager));
        console.log("Router:", address(router));
        vm.stopBroadcast();
    }
}

Deploy:

forge script script/DeployUniswapV3.s.sol:DeployUniswapV3 \
    --rpc-url $RPC_URL \
    --private-key $PRIVATE_KEY \
    --broadcast

Step 4: Verify Everything Works

Create a test script to verify pool creation and interaction:

// script/VerifyDeployment.s.sol
pragma solidity ^0.7.6;
pragma abicoder v2;
import "forge-std/Script.sol";
import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol";
import "@uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol";
contract VerifyDeployment is Script {
    function run() external view {
        address factory = 0x51cCFF9Fb59f3453faf09218fc6A932BEAD20A46; // Your factory
        address positionManager = 0x0C82F1f05387270B761756fD91ad8b867DEd8a0d; // Your PM
        address token0 = 0x...; // Your token0
        address token1 = 0x...; // Your token1
        uint24 fee = 3000;
        // Get pool from factory
        address poolFromFactory = IUniswapV3Factory(factory).getPool(
            token0,
            token1,
            fee
        );
        console.log("Pool from Factory:", poolFromFactory);
        // The Position Manager should compute the same address
        // If it doesn't, you still have the hash mismatch
    }
}

Real-World Deployment Example

Here’s what a complete deployment looks like in practice:

# 1. Calculate init code hash
forge script script/GetPoolInitCodeHash.s.sol:GetPoolInitCodeHash
# Output: 0x1100a600ace17973c8c8ccf7d2f8897b43d8a4eba420a7b5d1477ea1649cbc10
# 2. Update PoolAddress.sol with the new hash
# 3. Deploy everything
forge script script/DeployUniswapV3.s.sol:DeployUniswapV3 \
    --rpc-url http://localhost:8545 \
    --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
    --broadcast
# Output:
# Factory: 0x51cCFF9Fb59f3453faf09218fc6A932BEAD20A46
# WETH: 0x95a0415788A89E3dC4511aca4F8AE0C7ca614C6f
# Position Manager: 0x0C82F1f05387270B761756fD91ad8b867DEd8a0d
# Router: 0x5462897301E5530BB7402e120Cda0be70F273eeF
# 4. Now you can interact with pools successfully!

Common Mistakes and Gotchas

1. Copying Hashes from Tutorials

Don’t do this:

// ❌ WRONG - This is someone else's hash
bytes32 internal constant POOL_INIT_CODE_HASH = 
    0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54;

Do this:

// ✅ CORRECT - Calculate your own hash
bytes32 initCodeHash = keccak256(type(UniswapV3Pool).creationCode);

2. Forgetting to Redeploy Periphery Contracts

Updating PoolAddress.sol doesn't magically fix already-deployed contracts. You must redeploy:

  • NonfungiblePositionManager
  • All Router contracts
  • All Quoter contracts

3. Using Different Compiler Settings

If you change your foundry.toml after calculating the hash, you need to recalculate:

# If you change these settings, recalculate the hash!
[profile.default]
solc_version = "0.7.6"
optimizer = true
optimizer_runs = 800  # ← Changing this changes the bytecode

4. Mixing Deployment Sources

Don’t deploy the Factory from source but try to use pre-deployed periphery contracts from mainnet. Either:

  • Deploy everything from source (with updated hash), OR
  • Use pre-deployed contracts for everything

Why This Matters for Different Networks

Deploying to L2s (Arbitrum, Optimism, Base)

Most L2s have official Uniswap V3 deployments you can use. However, if you’re:

  • Customizing the contracts
  • Deploying to a new L2
  • Running a local L2 node for testing

You’ll need to handle the init code hash correctly.

Deploying to Testnets

Many testnets have official deployments, but not all. If you’re deploying from source, follow this guide.

Local Development with Anvil/Hardhat

This is where the issue appears most frequently. Every time you restart your local node and redeploy, follow the four-step process.

Debugging Tips

If you’re still having issues:

1. Verify Pool Address Calculation

// In your test/script
bytes32 salt = keccak256(abi.encode(token0, token1, fee));
address expectedPool = address(uint160(uint256(keccak256(abi.encodePacked(
    hex'ff',
    factory,
    salt,
    POOL_INIT_CODE_HASH
)))));
address actualPool = factory.getPool(token0, token1, fee);
console.log("Expected:", expectedPool);
console.log("Actual:", actualPool);
// These should match!

2. Check Factory Events

// Look for PoolCreated events
IUniswapV3Factory(factory).PoolCreated(...);

3. Verify Bytecode Hash

# In Foundry
forge inspect UniswapV3Pool bytecode | cast keccak

Best Practices for Uniswap V3 Deployments

1. Document Your Hash

In your README:

## Deployment
This deployment uses:
- Solidity: 0.7.6
- Optimizer: enabled
- Optimizer Runs: 800
- POOL_INIT_CODE_HASH: 0x1100a600ace17973c8c8ccf7d2f8897b43d8a4eba420a7b5d1477ea1649cbc10

2. Use Environment Variables

# .env
POOL_INIT_CODE_HASH=0x1100a600ace17973c8c8ccf7d2f8897b43d8a4eba420a7b5d1477ea1649cbc10
FACTORY_ADDRESS=0x51cCFF9Fb59f3453faf09218fc6A932BEAD20A46
POSITION_MANAGER=0x0C82F1f05387270B761756fD91ad8b867DEd8a0d
ROUTER_ADDRESS=0x5462897301E5530BB7402e120Cda0be70F273eeF

3. Automate the Process

Create a comprehensive deployment script that:

  1. Calculates the hash
  2. Updates PoolAddress.sol
  3. Deploys all contracts
  4. Verifies everything
  5. Saves addresses to a file

4. Version Control Your Modifications

# Track changes to v3-periphery
git diff lib/v3-periphery/contracts/libraries/PoolAddress.sol

The Technical Deep Dive

For those interested in why the bytecode changes, here are the factors:

Compiler Version Effects

Different Solidity versions generate different bytecode for the same source code:

  • Bug fixes change code generation
  • Optimizations improve between versions
  • ABI encoding changes

Optimizer Settings

The optimizer setting has massive effects:

optimizer = true          # vs false
optimizer_runs = 200      # vs 800 vs 1000000

More optimization runs = bytecode optimized for frequent execution = different bytecode.

Metadata Hash

Solidity appends metadata to bytecode by default:

// This gets appended to bytecode
{
    "compiler": { "version": "0.7.6+commit.7338295f" },
    "sources": { ... }
}

This metadata includes source file paths, which means compiling on different machines can produce different bytecode!

To disable: bytecode_hash = "none" in foundry.toml

Conclusion

The POOL_INIT_CODE_HASH mismatch is a rite of passage for Uniswap V3 developers. While frustrating, understanding it deeply gives you insight into:

  • How CREATE2 works
  • Why deterministic addressing matters
  • The relationship between bytecode and deployment
  • How periphery contracts interact with core contracts

Key Takeaways:

  1. Always calculate your own init code hash when deploying from source
  2. Update PoolAddress.sol with your calculated hash
  3. Redeploy all periphery contracts after updating the hash
  4. Document your deployment configuration
  5. Verify everything works before moving to production

Now you’re equipped to deploy Uniswap V3 anywhere — local nodes, testnets, or new L2s — without hitting the dreaded “call to non-contract address” error from your NonfungiblePositionManager.

Additional Resources


메타데이터
post_id
2ae7bef2204e
slug
nftpositionmanager-calling-to-non-contract-address-solving-uniswap-v3s-most-frustrating-error-2ae7bef2204e
url
https://medium.com/@amlanroy2020/nftpositionmanager-calling-to-non-contract-address-solving-uniswap-v3s-most-frustrating-error-2ae7bef2204e
canonical_url
https://medium.com/@amlanroy2020/nftpositionmanager-calling-to-non-contract-address-solving-uniswap-v3s-most-frustrating-error-2ae7bef2204e
author_url
https://medium.com/@amlanroy2020
status
ok
fetched_at
2026-08-20 18:19:56