> For the complete documentation index, see [llms.txt](https://docs.kpk.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.kpk.io/funds/infrastructure/architecture/onchain-accounting/meta-balance-adapters/uniswap-v3.md).

# 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 onchain: `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](/funds/infrastructure/architecture/onchain-accounting.md)
* **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

<figure><picture><source srcset="https://1699303810-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUsQM4bzcmOPSgkWddkeN%2Fuploads%2FcWE2qH58rMeAIqW2OQ2U%2Fmeta-uniswap-v3_D.png?alt=media&#x26;token=bc7d88da-8d1d-4a6c-a3a0-7011a8add622" media="(prefers-color-scheme: dark)"><img src="https://1699303810-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUsQM4bzcmOPSgkWddkeN%2Fuploads%2FNsqKyFcaqZee2al5GMCi%2Fmeta-uniswap-v3.png?alt=media&#x26;token=04beef6d-953c-4891-a738-dd5b0d0e27ff" alt="Flowchart of the Uniswap V3 balance adapter: read calls derive position legs into PositionBalance entries."></picture><figcaption><p>Uniswap V3 adapter: how the underlying balances reported to the NAV Calculator are derived.</p></figcaption></figure>

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(...)`:

```
(liq0, liq1, fee0, fee1) = UniswapV3PoolLib.positionAmounts(
    pool, position, sqrtPriceX96, tick    // sqrtPriceX96, tick from pool.slot0()
)
```

**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 onchain withdrawable split, not an oracle reconstruction.

{% hint style="info" %}
**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.
{% endhint %}

**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

```solidity
constructor(
    address positionManager_,
    address navCalculator_
)
```

| 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.

{% hint style="info" %}
**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.
{% endhint %}

## 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](/funds/infrastructure/architecture/onchain-accounting.md).

## Chains

| Chain        | Adapter                                                                                                                  | NonfungiblePositionManager                               | Pools configured |
| ------------ | ------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- | ---------------- |
| Ethereum (1) | [`0x2F7e8f78FD8EE977F6D53752b03A79c77012Ba52`](https://etherscan.io/address/0x2F7e8f78FD8EE977F6D53752b03A79c77012Ba52)  | `0xC36442b4a4522E871399CD717aBDD847Ab11FE88` (canonical) | 4                |
| Gnosis (100) | [`0xaC47D9Cbe8B3EDc9c9d1a6c8092054299bBC1adE`](https://gnosisscan.io/address/0xaC47D9Cbe8B3EDc9c9d1a6c8092054299bBC1adE) | `0xAE8fbE656a77519a7490054274910129c9244FA3`             | 7                |

The adapter takes the NPM as its only protocol argument, so its CREATE2 address is a pure function of that address: **one shared address across every chain hosting the canonical NPM, and a different one wherever the NPM differs.** Gnosis is the second case, hence two rows above rather than one. See [Deployment addresses](https://docs.kpk.io/funds/infrastructure/onchain-accounting/deployment-addresses#adapters-sharing-an-address-only-where-the-protocol-does).

{% hint style="danger" %}
**Gnosis hosts three Uniswap V3 deployments; identify the NPM by its `factory()`, never by `symbol()`.** All three NPMs answer `symbol() == "UNI-V3-POS"` and `supportsInterface(ERC721Enumerable) == true`, and all three were deployed by the same address, so neither the symbol nor the interface distinguishes them. Two are dead (`totalSupply == 0`): `0xd6053798…7537` behind factory `0x581e2A6e…`, and `0x49B53C35…2C71` behind factory `0x963Df249…`. The live one is `0xAE8fbE65…4FA3`, whose `factory()` is `0xe32F7dD7e3f098D518ff19A22d5f028e076489B1`, the address the [Gnosis analytics docs](https://docs.analytics.gnosis.io/protocols/dexes/uniswap-v3/) publish.
{% endhint %}

### Choosing the pool set

**Both tokens of a pool must be registered assets, 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. Beyond that, the set should stay tight: `_instancePositions` re-enumerates an account's NFTs **once per configured pool**, so the read cost is `O(pools × NFTs)` and an empty pool is pure overhead on every NAV read for every account on that chain.

The Gnosis set was chosen by querying the factory for every pair × fee-tier combination of the chain's registered assets, 24 live pools, and keeping the 7 holding more than $10k, which together account for essentially all of the \~$3.4M across them. Adding one of the rest later is an `addMetaInstances` transaction, not a deploy, and the omission direction is safe: a position in an unconfigured pool is **skipped, never over-valued**.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.kpk.io/funds/infrastructure/architecture/onchain-accounting/meta-balance-adapters/uniswap-v3.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
