@revxchain/uniswap-v4-utils
v1.0.0
Published
Solidity library with various utility functions for UniswapV4 interactions
Downloads
10
Maintainers
Readme
UniswapV4 Utils
A Solidity library providing various utility functions for UniswapV4 interactions — spot price quotes, TWAP quotes (via hook oracle), sqrtPriceX96 calculations, calculations of amounts for providing liquidity and existing position, tick aligners and accumulated fee getters.
Table of Contents
- Overview
- Main Features
- Project Structure
- Key Components
- Installation
- Quick Start
- Security Considerations
- License
- Contributing
- Support
Overview
UniswapV4Utils is a Solidity library that contains the common math operations and getters needed when integrating with the UniswapV4 protocol.
It consolidates spot price quotes, TWAP quotes (if a suitable hook implementing the IGeomeanOracle interface is available), sqrtPriceX96 calculations (using amounts, lowerSqrtPriceX96, or upperSqrtPriceX96), calculations of amounts for providing liquidity (compute proportional amounts) and existing position (compute current position's principal), tick aligners (by sqrtPriceX96 or tick) and accumulated fee getters (unclaimed and total earned) into a single module.
It also includes Hardhat Fixtures for full local deployment of the UniswapV4 protocol and a setup for testing integration using a mainnet fork (as well as combined V3 + V4 for both).
Main Features
| Feature | Function |
|---|---|
| TWAP quote with custom window and force flag | getTimeWeightedAmountOut(poolManager, poolKey, tokenIn, amountIn, secondsAgo, force, observeGasLimit) |
| Spot price quote | getAmountOut(poolManager, poolKey, tokenIn, amountIn) |
| Upper sqrtPriceX96 boundary for a position | getUpperSqrtPriceX96(lowerSqrtPriceX96, currentSqrtPriceX96, amount0, amount1) |
| Lower sqrtPriceX96 boundary for a position | getLowerSqrtPriceX96(currentSqrtPriceX96, upperSqrtPriceX96, amount0, amount1) |
| sqrtPriceX96 from tokens amounts ratio | getSqrtPriceX96(amount0, amount1) |
| Nearest valid tick from sqrtPriceX96 | getValidTick(sqrtPriceX96, tickSpacing) |
| Nearest valid tick from tick | getValidTick(tick, tickSpacing) |
| Proportional token amounts for a providing liquidity | getProportionalAmounts(poolManager, poolKey, amount0, amount1, tickLower, tickUpper) |
| Liquidity units from token amounts | getLiquidityForAmounts(poolManager, poolKey, tickLower, tickUpper, amount0, amount1) |
| Principal token amounts in existing position | getPositionPrincipal(poolManager, positionManager, tokenId) |
| Unclaimed swap fees | getUnclaimedFees(poolManager, positionManager, tokenId) |
| Total earned swap fees | getTotalEarnedFees(poolManager, positionManager, tokenId) |
Project Structure
uniswap-v4-utils/
├── src/
│ ├── UniswapV4Utils.sol # Main Solidity library
│ │
│ ├── interfaces/
│ │ ├── IPositionManagerTyped.sol # Struct-typed PositionManager interface
│ │ └── IGeomeanOracle.sol # GeomeanOracle hook interface
│ │
│ └── mocks/
│ ├── ERC20Token.sol # Test ERC20 token
│ ├── UniswapV4UtilsMock.sol # UniswapV4Utils wrapper
│ └── GeomeanOracle/ # Test GeomeanOracle hook
│ ├── GeomeanOracle.sol
│ ├── BaseHook.sol
│ ├── ImmutableState.sol
│ ├── Oracle.sol
│ └── SafeCallback.sol
│
├── test/
│ ├── UniswapV4DeploymentFixture.js # Full UniswapV4 local deployment + mainnet fork setup
│ ├── UniswapV3V4DeploymentFixture.js # Combined Uniswap V3 + V4 local deployment + mainnet fork setup
│ ├── UniswapV4UtilsFixture.js # Fixture for library test
│ ├── UniswapV4UtilsTest.js # Main test suite
│ ├── FixturesTest.js # Fixture tests
│ └── TestUtils.js # Test helpers
│
├── artifacts/ # Compiled contract artifacts
├── coverage/ # Code coverage reports
├── .github/ # Github CI files
├── env.example # Example of .env file
├── uniswap.addresses.json # UniswapV4 deployments per chain
├── hardhat.config.js # Hardhat configuration
├── .solcover.js # Solidity coverage configuration
├── slither.config.json # Slither analyzer config for CI
├── package.json # Dependencies and package info
└── README.md # This fileKey Components
UniswapV4Utils.sol
The primary Solidity library. Relies on the following math libraries:
UniswapV3Utils,OracleLibrary,LiquidityAmounts— UniswapV3 core math utilities from @revxchain/uniswap-v3-utils.FixedPoint128— fixed-point Q128 constant from @uniswap/v4-core.StateLibrary— pool state reader from @uniswap/v4-core.TickMath— UniswapV4 tick math from @uniswap/v4-core.FullMath— overflow-safemulDivfrom @uniswap/v4-core.Currency,PoolKey— UniswapV4 types from @uniswap/v4-core.
UniswapV4 Deployment Fixture
Hardhat fixture that deploys the complete UniswapV4 protocol from pre-compiled artifacts:
| Contract | Source |
|---|---|
| WETH9 | github/gnosis/canonical-weth/build/gnosis/canonical-weth/ |
| PoolManager | @uniswap/v4-core/out/ |
| PositionDescriptor | @uniswap/v4-periphery/foundry-out/ |
| PositionManager | @uniswap/v4-periphery/foundry-out/ |
| V4Quoter | @uniswap/v4-periphery/foundry-out/ |
| StateView | @uniswap/v4-periphery/foundry-out/ |
| Permit2 | @revxchain/uniswap-v3-utils/build/@uniswap/permit2/ |
| UniversalRouter | @uniswap/universal-router/artifacts |
UniswapV4 Deployment Fixture custom WETH
Hardhat fixture creation function that deploys the complete UniswapV4 protocol from compiled artifacts with custom WETH contract.
UniswapV4 Mainnet Fork Setup
The function uses uniswap.addresses.json to attach to live deployments for mainnet fork testing.
UniswapV3-V4 Deployment Fixture
Hardhat fixture that deploys both the complete UniswapV3 and UniswapV4 protocols simultaneously, sharing a single WETH and Permit2 instance, and deploying a unified UniversalRouter that routes through both protocol versions.
UniswapV3-V4 Deployment Fixture custom WETH
Hardhat fixture creation function that deploys both UniswapV3 and UniswapV4 protocols with custom WETH contract.
UniswapV3-V4 Mainnet Fork Setup
The function attaches to live UniswapV3 and UniswapV4 deployments for mainnet fork testing using uniswap.addresses.json for V4 contracts and uniswap.addresses.json for V3.
V4 Supported networks
| Network | Chain ID | |---|---| | Ethereum | 1 | | Unichain | 130 | | Arbitrum | 42161 | | Optimism | 10 | | Polygon | 137 | | Base | 8453 | | BSC | 56 | | Avalanche | 43114 | | Celo | 42220 | | Blast | 81457 | | Zora | 7777777 | | Monad | 143 | | MegaETH | 4326 | | WorldChain | 480 | | Ink | 57073 | | Soneium | 1868 |
[!WARNING] For networks where Ether is not the native asset, the
WETHcontract is used regardless. In reality, depends on the network; this is the wrapped version of the native asset.
[!WARNING] V3 and V4 deployments has a unique set of supported networks and contracts. Must familiarize with the V4 deployments and V3 deployments of the selected network before using it. For unsupported networks or/and contracts, zero-address placeholders will be used.
Installation
Repository
Step 1: Clone the repository
git clone https://github.com/RevxChain/uniswap-v4-utils.git
cd uniswap-v4-utilsStep 2: Install dependencies
npm installStep 3: Create environment file (optional)
cp .env.example .envEdit .env with your configuration:
# PRIVATE KEYS
PRIVATE_KEY = your_private_key_here
# MAINNET FORK TEST
FORK_RPC_URL = https://eth.llamarpc.com
FORK_BLOCK_NUMBER = 24540000
# RPC URLS
ETH_RPC_URL = https://eth.llamarpc.comStep 4: Compile contracts
npx hardhat compileCompiles all Solidity contracts and generates artifacts.
Step 5: Run tests
npx hardhat testStep 6: Run coverage
npx hardhat coverageStep 7: Run tests with custom fuzzing runs
Set fuzzing runs value in the hardhat.config.js:
fuzzing: {
enabled: true,
runs: 100
},Run tests:
npx hardhat testRun coverage:
npx hardhat coveragePackage
npm install @revxchain/uniswap-v4-utilsQuick Start
Solidity library:
UniswapV4Utils.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {UniswapV4Utils} from "@revxchain/uniswap-v4-utils/src/UniswapV4Utils.sol";
contract MyContract {
/// @notice Calculates the output amount for a token swap using a custom TWAP observation
/// window with optional force fallback.
function getTimeWeightedAmountOut(
IPoolManager poolManager,
PoolKey memory poolKey,
address tokenIn,
uint256 amountIn,
uint32 secondsAgo,
bool force,
uint256 observeGasLimit
) external view returns(uint256 amountOut) {
return UniswapV4Utils.getTimeWeightedAmountOut(
poolManager,
poolKey,
tokenIn,
amountIn,
secondsAgo,
force,
observeGasLimit
);
}
/// @notice Calculates the output amount for a token swap at the current spot price.
function getAmountOut(
IPoolManager poolManager,
PoolKey memory poolKey,
address tokenIn,
uint256 amountIn
) external view returns(uint256 amountOut) {
return UniswapV4Utils.getAmountOut(poolManager, poolKey, tokenIn, amountIn);
}
/// @notice Calculates the upper price boundary (sqrtPriceX96) for a liquidity position.
function getUpperSqrtPriceX96(
uint160 lowerSqrtPriceX96,
uint160 currentSqrtPriceX96,
uint256 amount0,
uint256 amount1
) external pure returns(uint160 upperSqrtPriceX96) {
return UniswapV4Utils.getUpperSqrtPriceX96(lowerSqrtPriceX96, currentSqrtPriceX96, amount0, amount1);
}
/// @notice Calculates the lower price boundary (sqrtPriceX96) for a liquidity position.
function getLowerSqrtPriceX96(
uint160 currentSqrtPriceX96,
uint160 upperSqrtPriceX96,
uint256 amount0,
uint256 amount1
) external pure returns(uint160 lowerSqrtPriceX96) {
return UniswapV4Utils.getLowerSqrtPriceX96(currentSqrtPriceX96, upperSqrtPriceX96, amount0, amount1);
}
/// @notice Calculates the effective square root of price (in Q64.96 format) from the ratio of two token
/// amounts.
function getSqrtPriceX96(uint256 amount0, uint256 amount1) external pure returns(uint160 sqrtPriceX96) {
return UniswapV4Utils.getSqrtPriceX96(amount0, amount1);
}
/// @notice Finds the nearest valid tick for a specified square root price, aligned to the tick spacing.
function getValidTick(uint160 sqrtPriceX96, int24 tickSpacing) external pure returns(int24 validTick) {
return UniswapV4Utils.getValidTick(sqrtPriceX96, tickSpacing);
}
/// @notice Finds the nearest valid tick for a specified tick, aligned to the specified tick spacing.
function getValidTick(int24 tick, int24 tickSpacing) external pure returns(int24 validTick) {
return UniswapV4Utils.getValidTick(tick, tickSpacing);
}
/// @notice Calculates the proportional amounts of both tokens required to provide liquidity within
/// a specified price range.
function getProportionalAmounts(
IPoolManager poolManager,
PoolKey memory poolKey,
uint256 amount0,
uint256 amount1,
int24 tickLower,
int24 tickUpper
) external view returns(uint256 amount0Required, uint256 amount1Required) {
return UniswapV4Utils.getProportionalAmounts(
poolManager,
poolKey,
amount0,
amount1,
tickLower,
tickUpper
);
}
/// @notice Calculates liquidity units from the specified token amounts for the specified price range.
function getLiquidityForAmounts(
IPoolManager poolManager,
PoolKey memory poolKey,
int24 tickLower,
int24 tickUpper,
uint256 amount0,
uint256 amount1
) external view returns(uint128 liquidity) {
return UniswapV4Utils.getLiquidityForAmounts(
poolManager,
poolKey,
tickLower,
tickUpper,
amount0,
amount1
);
}
/// @notice Calculates the actual token amounts (amount0 and amount1) represented by a position's liquidity.
function getPositionPrincipal(
IPoolManager poolManager,
address positionManager,
uint256 tokenId
) external view returns(uint256 amount0, uint256 amount1) {
return UniswapV4Utils.getPositionPrincipal(poolManager, positionManager, tokenId);
}
/// @notice Calculates the unclaimed swap fees for the specified liquidity position.
function getUnclaimedFees(
IPoolManager poolManager,
address positionManager,
uint256 tokenId
) external view returns(uint256 amount0, uint256 amount1) {
return UniswapV4Utils.getUnclaimedFees(poolManager, positionManager, tokenId);
}
/// @notice Calculates the total earned swap fees for the specified liquidity position.
function getTotalEarnedFees(
IPoolManager poolManager,
address positionManager,
uint256 tokenId
) external view returns(uint256 amount0, uint256 amount1) {
return UniswapV4Utils.getTotalEarnedFees(poolManager, positionManager, tokenId);
}
}Hardhat test environment:
UniswapV4DeploymentFixture()
const { loadFixture } = require("@nomicfoundation/hardhat-network-helpers");
const { UniswapV4DeploymentFixture } = require("@revxchain/uniswap-v4-utils/test/UniswapV4DeploymentFixture.js");
const {
uniswapV4Deployer, weth, poolManager, positionDescriptor, positionManager, quoter, stateView, permit2, universalRouter
} = await loadFixture(UniswapV4DeploymentFixture);createUniswapV4DeploymentFixtureCustomWETH()
const { loadFixture } = require("@nomicfoundation/hardhat-network-helpers");
const { createUniswapV4DeploymentFixtureCustomWETH } = require("@revxchain/uniswap-v4-utils/test/UniswapV4DeploymentFixture.js");
// your custom WETH's address
const wethAddress = "0x...";
// call function with custom WETH's address
const UniswapV4DeploymentFixtureCustomWETH = createUniswapV4DeploymentFixtureCustomWETH(wethAddress);
// initialize fixture
const {
uniswapV4Deployer, weth, poolManager, positionDescriptor, positionManager, quoter, stateView,
permit2, universalRouter
} = await loadFixture(UniswapV4DeploymentFixtureCustomWETH);UniswapV4MainnetForkSetup()
const { UniswapV4MainnetForkSetup } = require("@revxchain/uniswap-v4-utils/test/UniswapV4DeploymentFixture.js");
const targetChainId = 1;
const {
weth, poolManager, positionDescriptor, positionManager, quoter, stateView, permit2, universalRouter
} = await UniswapV4MainnetForkSetup(targetChainId);UniswapV3V4DeploymentFixture()
const { loadFixture } = require("@nomicfoundation/hardhat-network-helpers");
const { UniswapV3V4DeploymentFixture } = require("@revxchain/uniswap-v4-utils/test/UniswapV3V4DeploymentFixture.js");
const {
uniswapFactory, descriptorLibrary, tokenDescriptor, nonfungiblePositionManager, swapRouter01, swapRouter02,
quoter01, quoter02, tickLens, multicall, multicall2, uniswapV4Deployer, weth, poolManager, positionDescriptor,
positionManager, quoter, stateView, permit2, universalRouter, uniswapV3PoolBytecode, uniswapV3PoolAbi
} = await loadFixture(UniswapV3V4DeploymentFixture);createUniswapV3V4DeploymentFixtureCustomWETH()
const { loadFixture } = require("@nomicfoundation/hardhat-network-helpers");
const { createUniswapV3V4DeploymentFixtureCustomWETH } = require("@revxchain/uniswap-v4-utils/test/UniswapV3V4DeploymentFixture.js");
// your custom WETH's address
const wethAddress = "0x...";
// call function with custom WETH's address
const UniswapV3V4DeploymentFixtureCustomWETH = createUniswapV3V4DeploymentFixtureCustomWETH(wethAddress);
// initialize fixture
const {
uniswapFactory, descriptorLibrary, tokenDescriptor, nonfungiblePositionManager, swapRouter01, swapRouter02,
quoter01, quoter02, tickLens, multicall, multicall2, uniswapV4Deployer, weth, poolManager, positionDescriptor,
positionManager, quoter, stateView, permit2, universalRouter, uniswapV3PoolBytecode, uniswapV3PoolAbi
} = await loadFixture(UniswapV3V4DeploymentFixtureCustomWETH);UniswapV3V4MainnetForkSetup()
const { UniswapV3V4MainnetForkSetup } = require("@revxchain/uniswap-v4-utils/test/UniswapV3V4DeploymentFixture.js");
const targetChainId = 1;
const {
uniswapFactory, descriptorLibrary, tokenDescriptor, nonfungiblePositionManager, swapRouter01, swapRouter02,
quoter01, quoter02, tickLens, multicall, multicall2, weth, poolManager, positionDescriptor, positionManager,
quoter, stateView, permit2, universalRouter, uniswapV3PoolBytecode, uniswapV3PoolAbi
} = await UniswapV3V4MainnetForkSetup(targetChainId);[!NOTE] For Uniswap V3-V4 fixtures
uniswapV4Deployeris actuallyuniswapV3Deployeraccount too.
Security Considerations
No input validation.
UniswapV4Utilsdoes not verify the correctness of any input. Passing an address that is not a validPoolManager, aPoolKeythat does not correspond to an existing pool, or atokenInthat is not one of the pool's currencies, may result in silent zero returns rather than a revert. You MUST validate inputs externally.TWAP requires a compatible hook. The
getTimeWeightedAmountOutfunction relies on a hook deployed atpoolKey.hooksthat implements theIGeomeanOracleinterface. If no such hook is available andforce=false, the call will revert. Withforce=true, it falls back to the spot price. You MUST ensure the hook matches the logic of the built-inUniswapV3.Oracleand ensure the hook is not malicious.Spot price susceptibility. Functions that use the current tick (
getAmountOut) or theforcefallback path are susceptible to price manipulation within the same block. Use with caution — never as the sole price oracle for financial decisions.Fixed-point rounding and discrete tick logic. All calculations use integer fixed-point arithmetic with
Q64.96precision. In cases of very wide ranges, very narrow ranges, or extremely disproportionate token amounts, computed boundaries (getUpperSqrtPriceX96,getLowerSqrtPriceX96,getProportionalAmounts,getSqrtPriceX96,getLiquidityForAmounts) may differ significantly from a benchmark. Always verify remaining amounts (dust) after using this calculation for interaction. If your requirements allow for off-chain calculations, it's better to do it that way. This library is useful when full on-chain autonomy and minimal risk of functionality blocking are required.Oracle observation history.
getTimeWeightedAmountOut(withoutforce) will revert if the hook does not have sufficient historical observations for the requestedsecondsAgowindow. Ensure adequate observation cardinality before relying on TWAP.Gas limit during observation. The
observeGasLimitparameter controls the gas allocated to thestaticcallto the hook'sobservefunction. Setting it too low will cause the call to fail silently (withforce=truefalling back to spot price quote). Set a reasonable gas limit to avoid unexpected fallback behaviour.
License
MIT License
Permission is hereby granted to use, copy, modify, and distribute this software freely.
See LICENSE file for full terms.
Contributing
Contributions welcome! Please:
- Fork the repository;
- Create a feature branch;
- Add tests for new functionality;
- Submit a pull request.
Support
- GitHub Issues - Report bugs and/or proposals.
