Uniswap V3 Adapter – Absinthe Docs
Skip to content

Uniswap V3 Adapter

DEX with NFT-based LP positions

What It Tracks

  • swap: Swap events (same as V2)
  • lp: NFT LP position holdings

Design Reasoning

Why is V3 more complex than V2?

UniV3 uses NFTs for LP positions:

  • Each position has a unique tokenId
  • Positions have tick ranges (concentrated liquidity)
  • Need to track IncreaseLiquidity and DecreaseLiquidity separately
Why track by factory instead of pool?

V3 can have many pools, so we:

  1. Listen for PoolCreated events from the factory
  2. Index all pools in Redis
  3. Filter swap events by indexed pools
Why use NFT Position Manager events?

The NonFungiblePositionManager (NFPM) is the entry point for:

  • Minting new positions (Transfer from 0x0)
  • Modifying positions (IncreaseLiquidity/DecreaseLiquidity)
  • Transferring positions (Transfer between users)

Manifest

export const manifest = {
  name: 'uniswap-v3',
  version: '0.0.1',
  chainArch: 'evm',
  trackables: {
    swap: {
      kind: 'action',
      quantityType: 'token_based',
      params: {
        factoryAddress: evmAddress('The Uniswap V3 factory address'),
        nonFungiblePositionManagerAddress: evmAddress('The NFPM address'),
      },
      assetSelectors: {
        swapLegAddress: evmAddress('The token0 or token1 address to price'),
      },
    },
    lp: {
      kind: 'position',
      quantityType: 'token_based',
      params: {
        factoryAddress: evmAddress('The Uniswap V3 factory address'),
        nonFungiblePositionManagerAddress: evmAddress('The NFPM address'),
      },
      // No assetSelectors — NFT tokenId uniquely identifies position
      requiredPricer: univ3lpFeed,
    },
  },
} as const satisfies Manifest;

Event Handling Architecture

buildSqdProcessor: (base) => {
  let processor = base;
 
  // 1. Track pool creation (always needed)
  processor = processor.addLog({
    address: Array.from(factoryAddrs),
    topic0: [poolCreatedTopic],
  });
 
  // 2. Track LP position events (if lp configured)
  if (config.lp.length > 0) {
    processor = processor.addLog({
      address: Array.from(nfpmAddrs),
      topic0: [transferTopic, increaseLiquidityTopic, decreaseLiquidityTopic],
    });
  }
 
  // 3. Track swap events (if swap configured)
  if (config.swap.length > 0) {
    processor = processor.addLog({
      topic0: [swapTopic],  // All pools, filter later
    });
  }
 
  return processor;
},
 
onLog: async ({ log, emitFns, redis, sqdRpcCtx }) => {
  // Handle PoolCreated — index the pool
  if (log.topic0 === poolCreatedTopic) {
    const { pool, token0, token1 } = univ3factoryAbi.events.PoolCreated.decode(log);
    await redis.sadd(POOL_INDEX_KEY, pool.toLowerCase());
    await redis.hset(`pool:${pool.toLowerCase()}:tokens`, {
      token0: token0.toLowerCase(),
      token1: token1.toLowerCase(),
    });
  }
 
  // Handle Swap — check if pool is from our factory
  if (log.topic0 === swapTopic) {
    const isOurPool = await redis.sismember(POOL_INDEX_KEY, log.address.toLowerCase());
    if (!isOurPool) return;
    // ... handle swap
  }
 
  // Handle LP events
  if (log.topic0 === increaseLiquidityTopic) { /* ... */ }
  if (log.topic0 === decreaseLiquidityTopic) { /* ... */ }
  if (log.topic0 === transferTopic) { /* ... */ }
}

Example Config

{
  "adapterConfig": {
    "adapterId": "uniswap-v3",
    "config": {
      "swap": [
        {
          "params": {
            "factoryAddress": "0xCdBCd51a5E8728E0AF4895ce5771b7d17fF71959",
            "nonFungiblePositionManagerAddress": "0xe43ca1dee3f0fc1e2df73a0745674545f11a59f5"
          },
          "assetSelectors": {
            "swapLegAddress": "0x4200000000000000000000000000000000000006"
          },
          "pricing": {
            "assetType": "erc20",
            "priceFeed": { "kind": "pegged", "usdPegValue": 4000 }
          }
        }
      ],
      "lp": [
        {
          "params": {
            "factoryAddress": "0xCdBCd51a5E8728E0AF4895ce5771b7d17fF71959",
            "nonFungiblePositionManagerAddress": "0xe43ca1dee3f0fc1e2df73a0745674545f11a59f5"
          },
          "pricing": {
            "assetType": "erc721",
            "priceFeed": {
              "kind": "univ3lp",
              "token0": { "priceFeed": { "kind": "coingecko", "id": "weth" } },
              "token1": { "priceFeed": { "kind": "pegged", "usdPegValue": 1 } }
            }
          }
        }
      ]
    }
  }
}

Config Fields

TrackableFieldDescription
swap/lpparams.factoryAddressUniV3 Factory contract
swap/lpparams.nonFungiblePositionManagerAddressNFPM contract
swapassetSelectors.swapLegAddressWhich token to price
lppricing.assetTypeShould be "erc721" for NFTs

Key Differences from V2

AspectV2V3
LP TokenERC20ERC721 (NFT)
Position IDPool addressNFT tokenId
TrackingTransfer events onlyTransfer + IncreaseLiquidity + DecreaseLiquidity
Pool DiscoveryDirect configFactory indexing

How to Modify for Your Protocol (e.g., PancakeSwap V3, Ichi)

Same architecture, different addresses?
  • Update factory and NFPM addresses in config
  • No code changes needed
Different event signatures?
  • Update ABI files and regenerate types
  • Event handlers stay the same pattern
Additional events?
  • Add new topics to buildSqdProcessor
  • Add new handlers in onLog