For the complete documentation index, see llms.txt. This page is also available as Markdown.

Uniswap V3

A meta-adapter that reports an account's Uniswap V3 concentrated-liquidity LP across every configured pool — the in-range liquidity (Supplied) and the full uncollected fees (Fees) of each NFT position

A meta-adapter that reports an account's Uniswap V3 concentrated-liquidity LP — both the in-range liquidity (Supplied) and the full uncollected fees (Fees) of every NFT position the account holds. Each position is an ERC-721 minted by the NonfungiblePositionManager (NPM), which is ERC721Enumerable, so the adapter discovers an account's positions entirely on-chain: balanceOf(account) + tokenOfOwnerByIndex, then positions(tokenId), keeping the NFTs that belong to each configured pool. A single immutable contract serves every pool; the active pool set is governed on NAVCalculator.

  • Type: Meta-adapter

  • protocolSubId: keccak256("uniswap-v3") (brand uniswap, ecosystem uniswap-v3)

  • Instance key: the Uniswap V3 pool address, widened to bytes32.

  • Source: UniswapV3PoolsMetaBalanceAdapter.sol


Positions returned

Per NFT the account holds in a configured pool, up to four legs (token0/token1 × liquidity/fees):

Leg
PositionKind
isDebt
isLocked
Description

token0 liquidity

Supplied

false

false

token0 amount of the position's in-range liquidity

token1 liquidity

Supplied

false

false

token1 amount of the position's in-range liquidity

token0 fees

Fees

false

false

Full uncollected token0 fees (settled tokensOwed0 + feeGrowthInside delta)

token1 fees

Fees

false

false

Full uncollected token1 fees (settled tokensOwed1 + feeGrowthInside delta)

Both pool tokens must be registered in NAVCalculator or the whole instance is dropped — NAV values an LP from the sum of its two legs, so a half-priced LP would silently understate it. Each pool's fan-out is isolated by the meta base's per-instance try/catch, so a bad/unfeeded instance discards only that pool, never the account's whole Uniswap V3 LP set. Legs with zero amount, or assets excluded by assetFilter, are dropped. isLocked is false for every leg — these are live LP positions, not a withdrawal queue. The NFT tokenId rides in each leg's positionInstanceId.

Per-account discovery is bounded by MAX_POSITIONS (1000). The count comes from the account's full NPM NFT balance (enumerated before the per-pool filter) and is attacker-inflatable — NPM positions are force-mintable and freely transferable — so the cap guards against a griefer spamming dust position NFTs to gas-exhaust the NAV read. Any position at enumeration index >= MAX_POSITIONS is omitted (an under-report); for any real holding the cap is a no-op.


Balance calculation

Flowchart of the Uniswap V3 balance adapter: read calls derive position legs into PositionBalance entries.
Uniswap V3 adapter — how the underlying balances reported to the NAV Calculator are derived.

For each configured pool, the adapter enumerates the account's NPM NFTs, keeps those whose (token0, token1, fee) matches the pool, reads NPM.positions(tokenId) and the pool's live slot0, and derives the legs through UniswapV3PoolLib.positionAmounts(...):

Liquidity legs (Supplied) — the in-range token0/token1 amounts of the position's liquidity over its tick range, computed from the pool's live slot0 price (sqrtPriceX96, tick) — i.e. the instantaneous on-chain withdrawable split, not an oracle reconstruction.

Why live slot0 is safe here. Under the async NAV-consumer model, a NAV reading is not settled against in the same transaction, so transiently imbalancing the pool to skew the reported amounts cannot be monetised — arbitrage restores the pool within the block, and the consumer never acts on the manipulated mid-read value. Reporting the real instantaneous split keeps the adapter consistent with how the Balancer/Curve LP adapters value pro-rata holdings.

Fee legs (Fees) — the full uncollected fees: the settled tokensOwed0 / tokensOwed1 credited at the position's last interaction plus the feeGrowthInside delta accrued since, computed from the pool's current tick and the position's tick range.


Identity

  • positionId: abi.encode(address pool) — the pool address (the instance coordinate). Each (token0, token1, fee) triple is its own deployed pool, so the pool address uniquely identifies pair + fee tier; all of an account's NFTs in the same pool share one positionId and sum.

  • positionInstanceId: bytes32(tokenId) — the NFT id, the ephemeral per-item handle (excluded from the identity key). It is not in positionId and not in labels.

  • positionKind: Supplied for liquidity legs, Fees for fee legs.

  • labels: there is no labels field on the position. The adapter implements positionLabels(positionId), which returns ["AMM Liquidity Pool", "<sym0>/<sym1> <fee>%"] (e.g. ["AMM Liquidity Pool", "USDC/WETH 0.3%"]) — token symbols + fee tier read live from the pool, surfaced only by the verbose reads.


Constructor

Parameter
Description

positionManager_

The per-chain canonical NonfungiblePositionManager (NPM) address — the chain-level constant, same for every V3 pool on the chain. Must be a contract

navCalculator_

NAVCalculator contract for asset-registry access. Must be a contract

The pool addresses are not pinned — they are the governed instance set on NAVCalculator.

Licensing. This adapter links the GPL-2.0-or-later Uniswap V3 math libraries (TickMath, LiquidityAmounts, FixedPoint128 via UniswapV3PoolLib), so the combined work is GPL-2.0-or-later — hence the file's GPL-2.0-or-later SPDX identifier rather than the repo's default BUSL-1.1.


Registration

Register the adapter once with addMetaBalanceAdapter(adapter, pools), then enable each pool with addMetaInstances(adapter, pools) and drop coverage with removeMetaInstances. The instance coordinate is the Uniswap V3 pool address widened to bytes32. The adapter holds no instance state — it reads its active set from NAV_CALCULATOR.getMetaInstances(address(this)). See Meta balance adapters.

Last updated