---
title: Uniswap V3 Periphery with EVM on Polkadot Hub
description: Deploy and test unmodified Uniswap V3 Periphery contracts, SwapRouter and NonfungiblePositionManager, on Polkadot Hub using standard Hardhat and TypeScript with the EVM execution path.
categories:
- Smart Contracts
- Tooling
url: https://docs.polkadot.com/smart-contracts/cookbook/eth-dapps/uniswap-v3/periphery-v3/
word_count: 3087
token_estimate: 5331
version_hash: sha256:ea1e3abde1d69d430de5dc85fd39007d541161188861762908805147cb3bb0a4
last_updated: '2026-06-04T16:06:19+00:00'
---

# Deploy Uniswap V3 Periphery with EVM

## Introduction

The [Uniswap V3 Periphery](https://developers.uniswap.org/docs/protocols/v3/overview) contracts provide the user-facing layer that sits on top of the [Uniswap V3 Core](/smart-contracts/cookbook/eth-dapps/uniswap-v3/core-v3/) 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](https://github.com/bluealloy/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](https://nodejs.org/) v22.0.0 or later and npm installed
- Basic understanding of [Solidity](https://www.soliditylang.org/) and TypeScript
- Familiarity with the [Hardhat](/smart-contracts/dev-environments/hardhat/) development environment
- Some test tokens to cover transaction fees, obtained from the [Polkadot faucet](https://faucet.polkadot.io/). See [Get Test Tokens](/smart-contracts/faucet/#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](/smart-contracts/cookbook/eth-dapps/uniswap-v3/core-v3/), 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:

    ```bash
    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:

    ```bash
    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:

    ```bash
    npx hardhat compile
    ```

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

    <div id="termynal" data-termynal>
      <span data-ty="input"><span class="file-path"></span>npx hardhat compile</span>
      <span data-ty>Generating typings for: 111 artifacts in dir: typechain-types for target: ethers-v6</span>
      <span data-ty>Successfully generated 172 typings!</span>
      <span data-ty>Compiled 112 Solidity files successfully (evm target: istanbul).</span>
    </div>
    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](https://v2.hardhat.org/hardhat-runner/docs/guides/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:

```bash
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:

```typescript title="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:

```typescript title="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:

```typescript title="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](/smart-contracts/cookbook/eth-dapps/uniswap-v3/core-v3/) 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:

```text
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](/smart-contracts/dev-environments/local-dev-node/) guide to set it up.

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

    ```bash
    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:

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

```bash
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:

<div id="termynal" data-termynal markdown>
  <span data-ty="input">npx hardhat ignition deploy ./ignition/modules/UniswapV3Periphery.ts --network polkadotTestnet</span>
  <span data-ty>✔ Confirm deploy to network polkadotTestnet (420420417)? … yes</span>
  <span data-ty>&nbsp;</span>
  <span data-ty>Hardhat Ignition 🚀</span>
  <span data-ty>&nbsp;</span>
  <span data-ty>Deploying [ UniswapV3PeripheryModule ]</span>
  <span data-ty>&nbsp;</span>
  <span data-ty>Batch #1</span>
  <span data-ty> Executed UniswapV3PeripheryModule#UniswapV3Factory</span>
  <span data-ty> Executed UniswapV3PeripheryModule#WETH9</span>
  <span data-ty>&nbsp;</span>
  <span data-ty>Batch #2</span>
  <span data-ty> Executed UniswapV3PeripheryModule#SwapRouter</span>
  <span data-ty> Executed UniswapV3PeripheryModule#NonfungiblePositionManager</span>
  <span data-ty>&nbsp;</span>
  <span data-ty>[ UniswapV3PeripheryModule ] successfully deployed 🚀</span>
  <span data-ty>&nbsp;</span>
  <span data-ty>Deployed Addresses</span>
  <span data-ty>&nbsp;</span>
  <span data-ty>UniswapV3PeripheryModule#UniswapV3Factory - 0x5FbDB2315678afecb367f032d93F642f64180aa3</span>
  <span data-ty>UniswapV3PeripheryModule#WETH9 - 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512</span>
  <span data-ty>UniswapV3PeripheryModule#SwapRouter - 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0</span>
  <span data-ty>UniswapV3PeripheryModule#NonfungiblePositionManager - 0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9</span>
  <span data-ty="input"><span class="file-path"></span></span>
</div>
## Where to Go Next

<div class="grid cards" markdown>

-   <span class="badge tutorial">Tutorial</span> __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.

    [:octicons-arrow-right-24: Get Started](/smart-contracts/cookbook/eth-dapps/uniswap-v3/core-v3/)

-   <span class="badge guide">Guide</span> __Hardhat on Polkadot__

    ---

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

    [:octicons-arrow-right-24: Reference](/smart-contracts/dev-environments/hardhat/)

-   <span class="badge guide">Guide</span> __Local Development Node__

    ---

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

    [:octicons-arrow-right-24: Set Up](/smart-contracts/dev-environments/local-dev-node/)

</div>
