Skip to content

Deploy Uniswap V3 Periphery with EVM

Introduction

The Uniswap V3 Periphery contracts provide the user-facing layer that sits on top of the Uniswap V3 Core Factory and Pool contracts. While V3 Core handles the low-level concentrated liquidity engine, the Periphery contracts expose the functions that users and applications interact with directly: executing token swaps through one or more pools and managing concentrated liquidity positions as ERC-721 NFTs.

This tutorial follows the EVM execution path. With EVM (powered by REVM, a Rust implementation of the Ethereum Virtual Machine), you deploy the same unmodified Solidity contracts using the same standard Hardhat toolchain you already know. No special compiler plugins, no contract rewrites, and no porting effort. If your project compiles with vanilla Hardhat, it runs on Polkadot Hub through EVM.

This tutorial walks you through cloning, compiling, testing, and deploying the Uniswap V3 Periphery contracts on Polkadot Hub using Hardhat and TypeScript. By the end, you will have a fully functioning SwapRouter and NonfungiblePositionManager deployed to the Polkadot Hub TestNet.

Prerequisites

Before starting, make sure you have:

  • Node.js v22.0.0 or later and npm installed
  • Basic understanding of Solidity and TypeScript
  • Familiarity with the Hardhat development environment
  • Some test tokens to cover transaction fees, obtained from the Polkadot faucet. See Get Test Tokens for a guide to using the faucet
  • A wallet with a private key for signing transactions
  • Basic understanding of how AMMs and liquidity pools work
  • Completion of the Uniswap V3 Core tutorial, as the Periphery contracts depend on the V3 Core contracts through a local package reference

Set Up the Project

Start by cloning the EVM Hardhat examples repository, which contains the Uniswap V3 Periphery project with a standard Hardhat and TypeScript configuration:

  1. Clone the repository, check out the pinned commit, and navigate to the Uniswap V3 Periphery project:

    git clone https://github.com/polkadot-developers/revm-hardhat-examples.git
    cd revm-hardhat-examples
    git checkout 96696ad15c3cf01b9168a71ad5114f27c34a8726
    cd uniswap-v3-periphery-hardhat/
    
  2. Install the required dependencies:

    npm install
    

    Note

    The Periphery project depends on the V3 Core contracts through a local file reference ("@uniswap/v3-core": "file:../uniswap-v3-core-hardhat"). The npm install command resolves this automatically from the sibling directory in the repository, which is why you cloned the full monorepo rather than just the Periphery subdirectory.

  3. Compile the contracts:

    npx hardhat compile
    

    If the compilation is successful, you should see output similar to the following:

    npx hardhat compile Generating typings for: 111 artifacts in dir: typechain-types for target: ethers-v6 Successfully generated 172 typings! Compiled 112 Solidity files successfully (evm target: istanbul).

    After running this command, the compiled artifacts (ABI and bytecode) appear in the artifacts directory.

Configure Secure Key Management

This project uses Hardhat Configuration Variables to manage private keys securely. Unlike .env files, configuration variables are stored outside your project directory and are never at risk of being committed to version control.

To set your private key for TestNet deployment, run:

npx hardhat vars set TESTNET_PRIVATE_KEY

When prompted, paste your private key. Hardhat stores it securely and makes it available through vars.get("TESTNET_PRIVATE_KEY") in the configuration file.

Warning

Keep your private key safe and never share it with anyone. If it is compromised, your funds can be stolen.

The hardhat.config.ts file references the variable conditionally, so the project works without it for local development:

hardhat.config.ts
polkadotTestnet: {
  url: "https://services.polkadothub-rpc.com/testnet",
  accounts: vars.has("TESTNET_PRIVATE_KEY")
    ? [vars.get("TESTNET_PRIVATE_KEY")]
    : [],
},

Note

You only need the TESTNET_PRIVATE_KEY variable when deploying to the Polkadot Hub TestNet. Local development against the development node does not require any key configuration.

V3-Specific Configuration

The Periphery project uses the same critical compiler setting as V3 Core: bytecodeHash is set to none, which excludes the metadata hash from the compiled bytecode. This is required so that the compiled UniswapV3Pool bytecode matches the hardcoded POOL_INIT_CODE_HASH constant in PoolAddress.sol. The Periphery contracts use this constant to compute pool addresses via CREATE2. If there is a mismatch, every swap and LP operation silently calls the wrong address:

hardhat.config.ts
const config: HardhatUserConfig = {
  solidity: {
    version: "0.7.6",
    settings: {
      optimizer: {
        enabled: true,
        runs: 800,
      },
      metadata: {
        // Exclude metadata hash to keep bytecode deterministic.
        // This matches the v3-core compilation settings so the compiled
        // UniswapV3Pool bytecode hash aligns with the hardcoded
        // POOL_INIT_CODE_HASH in PoolAddress.sol.
        bytecodeHash: "none",
      },
    },
  },

The configuration also sets allowUnlimitedContractSize: true for the local Hardhat network, which is required because several Periphery contracts exceed the standard 24KB EIP-170 size limit. For the localNode network, a fixed gas price of 50 gwei matches the gas price reported by the Polkadot local development node:

hardhat.config.ts
networks: {
  hardhat: {
    allowUnlimitedContractSize: true,
  },
  localNode: {
    url: "http://127.0.0.1:8545",
    gasPrice: 50_000_000_000, // 50 gwei — matches Polkadot local node reported gas price
  },

Uniswap V3 Periphery Architecture

Before interacting with the contracts, it is essential to understand how the Periphery layer extends the V3 Core system. While the V3 Core contracts implement the concentrated liquidity engine, the Periphery contracts translate that engine into safe and ergonomic interfaces for end users and integrating applications.

Concentrated Liquidity and LP Positions

Uniswap V3 allows liquidity providers to concentrate capital within a chosen price range defined by a lower tick and an upper tick. A position earns trading fees only when the current pool price falls within its range.

The token composition of an out-of-range position is determined by its relationship to the current price. When the current price is below the position's range, the position holds 100% token0 because all liquidity has been converted to the cheaper asset as the price moved down through the range. When the current price is above the range, the position holds 100% token1 because all liquidity has been converted to the more expensive asset as the price moved up. Only an in-range position holds both tokens simultaneously.

Accumulated fees are tracked separately from principal liquidity and are staged in tokensOwed fields on the position. Retrieving fees requires two explicit steps: calling decreaseLiquidity to move accrued principal and fees into tokensOwed, then calling collect to transfer those amounts to the owner's wallet.

SwapRouter

The SwapRouter contract routes token swaps through one or more V3 pools. It supports four swap modes:

  • exactInputSingle: Spend a fixed amount of one token to receive as much of another token as possible through a single pool. The amountOutMinimum parameter enforces a slippage floor, and sqrtPriceLimitX96 optionally caps how far the price can move (enabling partial fills).
  • exactInput: Execute a multi-hop swap along an ABI-encoded path of pools. Each hop specifies a tokenIn, fee, and tokenOut. The full input amount is consumed and the final output must meet amountOutMinimum.
  • exactOutputSingle: Buy a precise amount of one token using as little of another token as possible through a single pool. The amountInMaximum parameter caps the input, and the router refunds any unused allowance.
  • exactOutput: Execute a multi-hop exact-output swap. The path is encoded in reverse (from the output token back to the input token), and the caller specifies the exact amount to receive.

NonfungiblePositionManager

The NonfungiblePositionManager (NFPM) represents each concentrated liquidity position as an ERC-721 NFT. A single contract manages the full LP lifecycle:

  • createAndInitializePoolIfNecessary: Creates a new V3 pool for a token pair and fee tier if none exists, and sets its initial price as a sqrtPriceX96 value. Calling this on an already-initialized pool is a safe no-op.
  • mint: Opens a new position within a specified tick range and mints an NFT to the recipient. The tokenId returned uniquely identifies the position. The amount0Min and amount1Min parameters guard against slippage during the initial deposit.
  • increaseLiquidity: Adds more capital to an existing position identified by its tokenId. The position's tick range and fee tier remain unchanged.
  • decreaseLiquidity: Removes a specified amount of liquidity from a position. Tokens are not transferred to the owner immediately; they are staged in the position's tokensOwed0 and tokensOwed1 fields. This two-step design allows the owner to decide when to withdraw.
  • collect: Transfers the amounts staged in tokensOwed (from both decreaseLiquidity and accumulated fees) to a specified recipient. The amount0Max and amount1Max parameters allow partial collection.
  • burn: Destroys the NFT for a position that has been fully exited. The position must have liquidity == 0 and tokensOwed0 == tokensOwed1 == 0; attempting to burn a live or uncollected position reverts.

Project Structure

The project scaffolding is as follows:

uniswap-v3-periphery-hardhat/
├── contracts/
│   ├── SwapRouter.sol                      # Swap router (single-hop + multi-hop)
│   ├── NonfungiblePositionManager.sol      # LP position NFT manager
│   ├── NonfungibleTokenPositionDescriptor.sol
│   ├── base/                               # Abstract base contracts
│   │   ├── PeripheryImmutableState.sol
│   │   ├── PeripheryPayments.sol
│   │   ├── LiquidityManagement.sol
│   │   ├── ERC721Permit.sol
│   │   ├── PoolInitializer.sol
│   │   └── ...
│   ├── libraries/                          # Pure utility libraries
│   │   ├── PoolAddress.sol
│   │   ├── Path.sol
│   │   ├── LiquidityAmounts.sol
│   │   └── ...
│   ├── interfaces/                         # Contract interfaces
│   │   ├── ISwapRouter.sol
│   │   ├── INonfungiblePositionManager.sol
│   │   └── ...
│   └── test/                              # Test helper contracts
│       ├── WETH9.sol
│       ├── TestERC20.sol
│       ├── MockTimeSwapRouter.sol
│       ├── MockTimeNonfungiblePositionManager.sol
│       └── CoreContracts.sol
├── ignition/
│   └── modules/
│       └── UniswapV3Periphery.ts          # Hardhat Ignition deployment module
├── scripts/
│   └── deploy.ts
├── test/
│   ├── SwapRouter.test.ts                 # Router tests (14 tests)
│   ├── NonfungiblePositionManager.test.ts # NFPM tests (25 tests)
│   └── shared/
│       ├── fixtures.ts
│       └── utilities.ts
├── hardhat.config.ts
├── package.json
├── tsconfig.json
└── README.md

Key differences from the V3 Core project are minimal. The Solidity contracts use the same version 0.7.6. The contracts/libraries/ directory contains periphery-specific math and path-encoding utilities such as PoolAddress.sol (deterministic CREATE2 address computation), Path.sol (multi-hop path encoding), and LiquidityAmounts.sol (token amount to liquidity unit conversion). The contracts/test/ directory includes CoreContracts.sol, an import shim that forces Hardhat to compile UniswapV3Factory and UniswapV3Pool from @uniswap/v3-core so their artifacts are available during tests. The test suite avoids loadFixture for compatibility with the Polkadot execution environment, using beforeEach with direct fixture calls instead.

Test the Contracts

The project includes a test suite with 39 tests across two test files:

  • SwapRouter.test.ts: 14 tests covering all four swap modes (exactInputSingle, exactInput, exactOutputSingle, exactOutput), slippage protection enforcement, recipient routing, sqrtPriceLimitX96 partial fills, and on-chain pool state changes after swaps.
  • NonfungiblePositionManager.test.ts: 25 tests covering the full LP lifecycle, including pool creation and initialization, minting in-range and out-of-range positions, increasing and decreasing liquidity, fee collection through real swaps, and the complete burn cleanup sequence.

To run the tests locally:

  1. Start the local development node. Follow the steps in the Local Development Node guide to set it up.

  2. In a new terminal, run the test suite against the local node:

    npx hardhat test --network localNode
    

    The tests are configured with a 120-second Mocha timeout to accommodate Polkadot network block times. The result should look similar to the following:

    npx hardhat test --network localNode Compiled 112 Solidity files successfully (evm target: istanbul). NonfungiblePositionManager ✔ constructor — returns correct factory and WETH9 addresses (298ms) #createAndInitializePoolIfNecessary ✔ creates a new pool and sets the initial sqrtPriceX96 (1234ms) ✔ is a no-op when the pool already exists and is initialized (987ms) #mint ✔ fails if pool does not exist (312ms) in-range (full-range) position success cases ✔ mints exactly one NFT to the recipient (2156ms) ✔ decreases token0 and token1 balances of the caller (2087ms) ✔ records correct position data in positions() (2243ms) ✔ emits IncreaseLiquidity event with positive liquidity (2178ms) failure cases ✔ reverts when amount0Min slippage is not satisfied (1123ms) out-of-range position (above current price) ✔ mints a single-sided token0 position — zero token1 consumed (2089ms) out-of-range position (below current price) ✔ mints a single-sided token1 position — zero token0 consumed (2134ms) #increaseLiquidity ✔ increases the position liquidity (2345ms) ✔ emits IncreaseLiquidity event (2276ms) #decreaseLiquidity success cases ✔ stages removed tokens in tokensOwed (not immediately transferred) (3234ms) ✔ can remove all liquidity from a position (3189ms) ✔ emits DecreaseLiquidity event (3156ms) failure cases ✔ reverts when called by a non-owner (2087ms) ✔ reverts when requested liquidity exceeds the position (2134ms) #collect tokensOwed from decreaseLiquidity ✔ transfers tokensOwed back to the owner (3567ms) ✔ emits Collect event (3489ms) ✔ collecting less than the maximum leaves remainder in tokensOwed (3623ms) swap fee accumulation ✔ LP collects accrued trading fees after swaps through the pool (7891ms) #burn ✔ burns the NFT after full removal and collection (4234ms) ✔ reverts when liquidity has not been fully removed (3123ms) ✔ reverts when tokensOwed have not been collected (3087ms) SwapRouter ✔ constructor — returns correct factory and WETH9 addresses (289ms) #exactInputSingle success cases ✔ swaps token0 for token1 via a single pool (2134ms) ✔ respects sqrtPriceLimitX96 and executes a partial fill (2087ms) ✔ sends output tokens to the specified recipient, not the caller (2156ms) failure cases ✔ reverts when output is below amountOutMinimum (1234ms) #exactInput success cases ✔ swaps token0 → token1 → token2 via two pools (3456ms) failure cases ✔ reverts when output is below amountOutMinimum (2345ms) #exactOutputSingle success cases ✔ buys an exact amount of token1 using token0 (2178ms) ✔ spends strictly less than amountInMaximum when pool has sufficient liquidity (2234ms) failure cases ✔ reverts when amountInMaximum is exceeded (1198ms) #exactOutput success cases ✔ buys an exact amount of token2 using token0 through two pools (3589ms) failure cases ✔ reverts when amountInMaximum is exceeded (multi-hop) (2456ms) pool price impact ✔ exactInputSingle moves the pool sqrtPriceX96 (2089ms) ✔ exactOutputSingle moves the pool sqrtPriceX96 (2134ms) 39 passing (2m)

Tip

If the tests fail due to a timeout, ensure your local development node is running and accessible at http://127.0.0.1:8545.

Deploy the Contracts

After successfully testing the contracts, you can deploy them to the Polkadot Hub TestNet using Hardhat Ignition. The Ignition module at ignition/modules/UniswapV3Periphery.ts deploys all four contracts: UniswapV3Factory, WETH9, SwapRouter, and NonfungiblePositionManager.

Make sure you have configured your private key and that your account has test tokens. Then run:

npx hardhat ignition deploy ./ignition/modules/UniswapV3Periphery.ts --network polkadotTestnet

When prompted, confirm the target network name and chain ID. Ignition deploys the contracts in two batches. It first deploys the Factory and WETH9 in parallel, then deploys the SwapRouter and NonfungiblePositionManager once their dependencies are available. It prints all deployed addresses. The output should look similar to the following:

npx hardhat ignition deploy ./ignition/modules/UniswapV3Periphery.ts --network polkadotTestnet ✔ Confirm deploy to network polkadotTestnet (420420417)? … yes   Hardhat Ignition 🚀   Deploying [ UniswapV3PeripheryModule ]   Batch #1 Executed UniswapV3PeripheryModule#UniswapV3Factory Executed UniswapV3PeripheryModule#WETH9   Batch #2 Executed UniswapV3PeripheryModule#SwapRouter Executed UniswapV3PeripheryModule#NonfungiblePositionManager   [ UniswapV3PeripheryModule ] successfully deployed 🚀   Deployed Addresses   UniswapV3PeripheryModule#UniswapV3Factory - 0x5FbDB2315678afecb367f032d93F642f64180aa3 UniswapV3PeripheryModule#WETH9 - 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 UniswapV3PeripheryModule#SwapRouter - 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0 UniswapV3PeripheryModule#NonfungiblePositionManager - 0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9

Where to Go Next

  • Tutorial Uniswap V3 Core


    Deploy the Uniswap V3 Factory and Pool contracts on Polkadot Hub to understand the concentrated liquidity engine that the Periphery builds on.

    Get Started

  • Guide Hardhat on Polkadot


    Learn how to create, compile, test, and deploy smart contracts on Polkadot Hub using Hardhat.

    Reference

  • Guide Local Development Node


    Set up and run a local development node for testing your smart contracts against Polkadot.

    Set Up

Last update: June 4, 2026
| Created: May 15, 2026