Deploy Uniswap V3 Core with EVM¶
Introduction¶
Polkadot Hub supports two execution paths for running smart contracts: PVM (which compiles Solidity to the Polkadot Virtual Machine via the revive compiler) and EVM (powered by REVM, a Rust implementation of the Ethereum Virtual Machine, which runs standard EVM bytecode with zero modifications). This tutorial follows the EVM path.
With EVM, 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 Uniswap V3 Core on Polkadot Hub using Hardhat and TypeScript. By the end, you will have a fully functioning UniswapV3Factory contract 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
Set Up the Project¶
Start by cloning the EVM Hardhat examples repository, which contains the Uniswap V3 Core project with a standard Hardhat and TypeScript configuration:
-
Clone the repository, check out the pinned commit, and navigate to the Uniswap V3 project:
-
Install the required dependencies:
-
Compile the contracts:
If the compilation is successful, you should see output similar to the following:
npx hardhat compile Generating typings for: 50 artifacts in dir: typechain-types for target: ethers-v6 Successfully generated 78 typings! Compiled 50 Solidity files successfully (evm target: istanbul).After running this command, the compiled artifacts (ABI and bytecode) appear in the
artifactsdirectory.
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:
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:
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¶
Uniswap V3 Core requires specific Solidity compiler settings to keep the UniswapV3Factory contract under the EIP-170 24KB contract size limit. The hardhat.config.ts file sets bytecodeHash to "none", which excludes the metadata hash from the compiled bytecode. This matches the original Uniswap V3 Core deployment configuration:
solidity: {
version: "0.7.6",
settings: {
optimizer: {
enabled: true,
runs: 800,
},
metadata: {
// Exclude metadata hash to keep contract size deterministic and under EIP-170 limit
// This matches the original Uniswap V3 Core deployment configuration
bytecodeHash: "none",
},
},
},
The configuration also sets a fixed gas price of 50 gwei for the localNode network to match the gas price reported by the Polkadot local development node:
localNode: {
url: "http://127.0.0.1:8545",
gasPrice: 50_000_000_000, // 50 gwei — matches Polkadot local node reported gas price
},
Uniswap V3 Core Architecture¶
Before interacting with the contracts, it is essential to understand the core architecture that powers Uniswap V3. This version introduces concentrated liquidity, a fundamentally different model from V2's uniform distribution that allows liquidity providers to allocate capital within specific price ranges for dramatically improved capital efficiency.
Concentrated Liquidity and Fee Tiers¶
In Uniswap V2, liquidity is spread uniformly across the entire price curve from zero to infinity. Uniswap V3 replaces this with concentrated liquidity, where providers choose a specific price range in which their capital is active. This means a position only earns fees when the current price falls within its selected range, but the capital within that range is far more effective.
Uniswap V3 also introduces multiple fee tiers so that pools can match the risk profile of different token pairs:
| Fee Tier | Fee Percentage | Tick Spacing | Typical Use Case |
|---|---|---|---|
| 500 | 0.05% | 10 | Stable pairs (e.g., USDC/DAI) |
| 3000 | 0.30% | 60 | Standard pairs (e.g., ETH/USDC) |
| 10000 | 1.00% | 200 | Exotic or volatile pairs |
Tick System and Price Math¶
Prices in Uniswap V3 are represented as the square root of the price ratio, stored as a fixed-point Q64.96 value (sqrtPriceX96). The continuous price space is divided into discrete ticks, where each tick represents a 0.01% (1 basis point) price change. Liquidity positions are bounded by a lower tick and an upper tick, and the protocol tracks which ticks have active liquidity through a bitmap data structure.
Core Contracts¶
At the heart of Uniswap V3 are four core smart contracts:
UniswapV3Factory: Creates and registers new pools. Each unique combination of two tokens and a fee tier produces a single pool. The Factory also controls protocol fee settings and ownership.UniswapV3Pool: The main contract for each trading pair and fee tier. It manages concentrated liquidity positions, executes swaps using the tick-based price system, and maintains a built-in TWAP (time-weighted average price) oracle.UniswapV3PoolDeployer: A helper contract used by the Factory to deploy new pools viaCREATE2, ensuring deterministic pool addresses.NoDelegateCall: A security base contract that prevents delegate call exploits by verifying the execution context matches the original deployment address.
Math Libraries¶
The V3 protocol relies on an extensive suite of 16 math library contracts for precise fixed-point arithmetic, tick calculations, and price computations. These include TickMath for converting between ticks and sqrt prices, SqrtPriceMath for computing swap amounts, FullMath for 512-bit multiplication, and Oracle for TWAP accumulator management, among others.
Project Structure¶
The project scaffolding is as follows:
uniswap-v3-core-hardhat/
├── contracts/
│ ├── interfaces/
│ │ ├── callback/
│ │ │ ├── IUniswapV3FlashCallback.sol
│ │ │ ├── IUniswapV3MintCallback.sol
│ │ │ └── IUniswapV3SwapCallback.sol
│ │ ├── pool/
│ │ │ ├── IUniswapV3PoolActions.sol
│ │ │ ├── IUniswapV3PoolDerivedState.sol
│ │ │ ├── IUniswapV3PoolEvents.sol
│ │ │ ├── IUniswapV3PoolImmutables.sol
│ │ │ ├── IUniswapV3PoolOwnerActions.sol
│ │ │ └── IUniswapV3PoolState.sol
│ │ ├── IERC20Minimal.sol
│ │ ├── IUniswapV3Factory.sol
│ │ ├── IUniswapV3Pool.sol
│ │ └── IUniswapV3PoolDeployer.sol
│ ├── libraries/
│ │ ├── BitMath.sol
│ │ ├── FixedPoint128.sol
│ │ ├── FixedPoint96.sol
│ │ ├── FullMath.sol
│ │ ├── LiquidityMath.sol
│ │ ├── LowGasSafeMath.sol
│ │ ├── Oracle.sol
│ │ ├── Position.sol
│ │ ├── SafeCast.sol
│ │ ├── SqrtPriceMath.sol
│ │ ├── SwapMath.sol
│ │ ├── Tick.sol
│ │ ├── TickBitmap.sol
│ │ ├── TickMath.sol
│ │ ├── TransferHelper.sol
│ │ └── UnsafeMath.sol
│ ├── test/
│ │ ├── BitMathTest.sol
│ │ ├── FullMathTest.sol
│ │ ├── LiquidityMathTest.sol
│ │ ├── MockTimeUniswapV3Pool.sol
│ │ ├── MockTimeUniswapV3PoolDeployer.sol
│ │ ├── NoDelegateCallTest.sol
│ │ ├── OracleTest.sol
│ │ ├── SqrtPriceMathTest.sol
│ │ ├── SwapMathTest.sol
│ │ ├── TestERC20.sol
│ │ ├── TestUniswapV3Callee.sol
│ │ ├── TestUniswapV3ReentrantCallee.sol
│ │ ├── TestUniswapV3Router.sol
│ │ ├── TestUniswapV3SwapPay.sol
│ │ ├── TickBitmapTest.sol
│ │ ├── TickMathTest.sol
│ │ └── TickTest.sol
│ ├── NoDelegateCall.sol
│ ├── UniswapV3Factory.sol
│ ├── UniswapV3Pool.sol
│ └── UniswapV3PoolDeployer.sol
├── ignition/
│ └── modules/
│ └── UniswapV3Factory.ts
├── scripts/
│ └── deploy.ts
├── test/
│ ├── shared/
│ │ ├── checkObservationEquals.ts
│ │ ├── fixtures.ts
│ │ ├── format.ts
│ │ └── utilities.ts
│ ├── UniswapV3Factory.test.ts
│ └── UniswapV3Pool.test.ts
├── hardhat.config.ts
├── package.json
└── tsconfig.json
Key differences from V2 are significant. The Solidity contracts use version 0.7.6 (V2 used 0.5.16). The contracts/libraries/ directory contains 16 math libraries for tick calculations, fixed-point arithmetic, and oracle management. The contracts/test/ directory includes 17 test helper contracts, including mock pools, routers, and math test harnesses. The test suite is split across two files instead of three, with a shared utilities directory.
Test the Contracts¶
The project includes a comprehensive test suite with 187 tests across two test files:
UniswapV3Factory.test.ts: 21 tests covering factory operations, pool creation, fee tier management, and ownership controls.UniswapV3Pool.test.ts: 166 tests covering concentrated liquidity positions, swaps across tick boundaries, fee accumulation, flash loans, oracle observations, and edge cases.
To run the tests locally:
-
Start the local development node. Follow the steps in the Local Development Node guide to set it up.
-
In a new terminal, run the test suite against the local node:
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 50 Solidity files successfully (evm target: istanbul). UniswapV3Factory ✔ owner is deployer ✔ initial enabled fee amounts #createPool ✔ succeeds for low fee pool (312ms) ✔ succeeds for medium fee pool (298ms) ✔ succeeds for high fee pool (301ms) ✔ succeeds if tokens are passed in reverse (287ms) ✔ fails if token a == token b ✔ fails if token a is 0 or token b is 0 ✔ fails if fee amount is not enabled #setOwner ✔ fails if caller is not owner ✔ updates owner (189ms) ✔ emits event (203ms) ✔ cannot be called by original owner #enableFeeAmount ✔ fails if caller is not owner ✔ fails if fee is too great ✔ fails if tick spacing is too small ✔ fails if tick spacing is too large ✔ fails if already initialized ✔ sets the fee amount in the mapping (211ms) ✔ emits an event (198ms) ✔ enables pool creation (267ms) UniswapV3Pool ✔ constructor initializes immutables #initialize ✔ fails if already initialized ✔ fails if starting price is too low ✔ fails if starting price is too high ✔ can be initialized at MIN_SQRT_RATIO (412ms) ✔ can be initialized at MAX_SQRT_RATIO - 1 (389ms) ✔ sets initial variables (401ms) ✔ initializes the first observations slot (445ms) ✔ emits a Initialized event with the input tick (378ms) #increaseObservationCardinalityNext ✔ fails if not initialized ✔ does not change cardinality next if less than current (356ms) ✔ increases cardinality next (498ms) ✔ emits an event (423ms) #mint after initialization ✔ fails if not initialized ✔ fails if tickLower greater than tickUpper (312ms) ✔ fails if tickLower less than min tick (298ms) ✔ fails if tickUpper greater than max tick (287ms) ✔ fails if amount is zero (401ms) ✔ mints within the range (15243ms) ✔ mints at the range lower edge (14987ms) ✔ mints at the range upper edge (15104ms) ✔ provides liquidity in both tokens when in range (16312ms) ✔ emits a Mint event (12876ms) #burn ✔ cannot burn more than position liquidity (398ms) ✔ burns partial liquidity (18934ms) ✔ burns entire liquidity (19201ms) ✔ emits a Burn event (18756ms) #observe ✔ fails if not initialized ✔ returns correct cumulative values (14523ms) ✔ interpolates correctly between observations (16891ms) #collect ✔ reverts if position has no tokens owed (312ms) ✔ collects token0 and token1 fees (21345ms) ✔ collects protocol fees (22109ms) #feeProtocol ✔ fails if caller is not factory owner ✔ sets fee protocol (19876ms) ✔ emits a SetFeeProtocol event (18543ms) 187 passing (42m)
Tip
If tests time out, 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/UniswapV3Factory.ts deploys the UniswapV3Factory contract.
Make sure you have configured your private key and that your account has test tokens. Then run:
When prompted, confirm the target network name and chain ID. Ignition deploys the Factory contract and prints the deployed address. The output should look similar to the following:
npx hardhat ignition deploy ./ignition/modules/UniswapV3Factory.ts --network polkadotTestnet ✔ Confirm deploy to network polkadotTestnet (420420417)? … yes Hardhat Ignition 🚀 Deploying [ UniswapV3FactoryModule ] Batch #1 Executed UniswapV3FactoryModule#UniswapV3Factory [ UniswapV3FactoryModule ] successfully deployed 🚀 Deployed Addresses UniswapV3FactoryModule#UniswapV3Factory - 0x2e234DAe75C793f67A35089C9d99245E1C58470b
Where to Go Next¶
-
Guide Hardhat on Polkadot
Learn how to create, compile, test, and deploy smart contracts on Polkadot Hub using Hardhat.
-
Tutorial Uniswap V3 Periphery
Deploy the SwapRouter and NonfungiblePositionManager contracts on Polkadot Hub to add token swaps and NFT-based liquidity position management on top of V3 Core.
-
Guide Local Development Node
Set up and run a local development node for testing your smart contracts against Polkadot.
| Created: April 23, 2026