Uniswap V2 Adapter – Absinthe Docs
Skip to content

Uniswap V2 Adapter

DEX with two trackables: swaps (actions) and LP positions

What It Tracks

  • swap: Individual swap events with volume
  • lp: LP token holdings over time

Design Reasoning

Why TWO trackables?
Swaps and LP positions are fundamentally different:

  • Swaps are one-time events (actions)
  • LP positions are ongoing balances (positions)
Why does swap have assetSelectors but lp doesn't?

For swaps: Each swap involves TWO tokens. You need to specify which one to price. The swapLegAddress selector tells us "price WETH volume" or "price USDC volume."

For LP: The pool address IS the LP token. There's only one asset to track. The univ2nav pricer handles the underlying tokens internally.

Why requiredPricer: univ2nav for LP?
LP tokens need special NAV calculation using:

  • Pool reserves
  • Underlying token prices (token0 + token1)

Standard pricers can't handle this, so we require the specialized univ2nav pricer.

Manifest

export const manifest = {
  name: 'uniswap-v2',
  version: '0.0.1',
  chainArch: 'evm',
  trackables: {
    swap: {
      kind: 'action',              // One-time events
      quantityType: 'token_based',
      params: {
        poolAddress: evmAddress('The pool address to track'),
      },
      assetSelectors: {
        // REQUIRED: Which token in the pair to price
        swapLegAddress: evmAddress('The token0 or token1 address to price'),
      },
    },
    lp: {
      kind: 'position',            // Ongoing balances
      quantityType: 'token_based',
      params: {
        poolAddress: evmAddress('The pool address to track'),
      },
      // No assetSelectors — pool address uniquely identifies LP token
      requiredPricer: univ2navFeed,  // Must use NAV calculation
    },
  },
} as const satisfies Manifest;

Event Handling (Swap)

export async function handleSwap(
  log: UnifiedEvmLog,
  emitFns: EmitFunctions,
  instance: InstanceFrom<typeof manifest.trackables.swap>,
  tk0Addr: string,
  tk1Addr: string,
): Promise<void> {
  // Decode the swap event
  const decoded = univ2Abi.events.Swap.decode({ topics: log.topics, data: log.data });
 
  // Determine swap direction
  const isToken0ToToken1 = decoded.amount0In > 0n;
  
  const fromAmount = isToken0ToToken1 ? decoded.amount0In : decoded.amount1In;
  const toAmount = isToken0ToToken1 ? decoded.amount1Out : decoded.amount0Out;
  const fromTokenAddress = isToken0ToToken1 ? tk0Addr : tk1Addr;
  const toTokenAddress = isToken0ToToken1 ? tk1Addr : tk0Addr;
 
  // Filter by asset selector (which side to track)
  const swapLegAddress = instance.assetSelectors?.swapLegAddress;
  const shouldEmitFromSide = !swapLegAddress || swapLegAddress === fromTokenAddress;
  const shouldEmitToSide = !swapLegAddress || swapLegAddress === toTokenAddress;
 
  // Emit helper
  const emitSwapSide = async (asset: string, amount: bigint) => {
    await emitFns.action.swap({
      key: md5Hash(`${log.txRef}${log.index}`),  // Deduplication key
      activity: 'swap',
      user: log.transactionFrom,
      asset: { type: 'erc20', address: asset },
      amount: amount,
      trackableInstance: instance,
    });
  };
 
  // Emit based on selector
  if (shouldEmitFromSide) await emitSwapSide(fromTokenAddress, fromAmount);
  if (shouldEmitToSide) await emitSwapSide(toTokenAddress, toAmount);
}

Event Handling (LP Transfer)

export async function handleLpTransfer(
  log: UnifiedEvmLog,
  emitFns: EmitFunctions,
  instance: InstanceFrom<typeof manifest.trackables.lp>,
  poolAddress: string,
): Promise<void> {
  const decoded = univ2Abi.events.Transfer.decode({ topics: log.topics, data: log.data });
 
  const fromAddress = decoded.from.toLowerCase();
  const toAddress = decoded.to.toLowerCase();
  const zeroAddress = '0x0000000000000000000000000000000000000000';
 
  // Decrease sender's LP balance (skip zero address = mints)
  if (fromAddress !== zeroAddress && fromAddress !== poolAddress.toLowerCase()) {
    await emitFns.position.balanceDelta({
      user: fromAddress,
      asset: { type: 'erc20', address: poolAddress },
      amount: -decoded.value,
      activity: 'hold',
      trackableInstance: instance,
    });
    await emitFns.position.reprice({ trackableInstance: instance });
  }
 
  // Increase receiver's LP balance (skip zero address = burns)
  if (toAddress !== zeroAddress && toAddress !== poolAddress.toLowerCase()) {
    await emitFns.position.balanceDelta({
      user: toAddress,
      asset: { type: 'erc20', address: poolAddress },
      amount: decoded.value,
      activity: 'hold',
      trackableInstance: instance,
    });
    await emitFns.position.reprice({ trackableInstance: instance });
  }
}

Example Config (Both Trackables)

{
  "adapterConfig": {
    "adapterId": "uniswap-v2",
    "config": {
      "swap": [
        {
          "params": { "poolAddress": "0x0621bae969de9c153835680f158f481424c0720a" },
          "assetSelectors": { "swapLegAddress": "0xAA40c0c7644e0b2B224509571e10ad20d9C4ef28" },
          "pricing": {
            "assetType": "erc20",
            "priceFeed": { "kind": "pegged", "usdPegValue": 115764.58 }
          }
        },
        {
          "params": { "poolAddress": "0x0621bae969de9c153835680f158f481424c0720a" },
          "assetSelectors": { "swapLegAddress": "0xad11a8BEb98bbf61dbb1aa0F6d6F2ECD87b35afA" },
          "pricing": {
            "assetType": "erc20",
            "priceFeed": { "kind": "pegged", "usdPegValue": 1 }
          }
        }
      ],
      "lp": [
        {
          "params": { "poolAddress": "0x0621bae969de9c153835680f158f481424c0720a" },
          "pricing": {
            "assetType": "erc20",
            "priceFeed": {
              "kind": "univ2nav",
              "token0": {
                "assetType": "erc20",
                "priceFeed": { "kind": "pegged", "usdPegValue": 115764.58 }
              },
              "token1": {
                "assetType": "erc20",
                "priceFeed": { "kind": "pegged", "usdPegValue": 1 }
              }
            }
          }
        }
      ]
    }
  }
}

Config Fields

TrackableFieldDescription
swapparams.poolAddressThe UniV2 pool contract
swapassetSelectors.swapLegAddressWhich token to price (token0 or token1)
lpparams.poolAddressThe UniV2 pool contract (= LP token)
lppricing.priceFeed.token0Pricing for underlying token0
lppricing.priceFeed.token1Pricing for underlying token1

Key Patterns

  1. Cache token addresses: Token0/token1 never change, so cache in Redis
  2. Deduplication key: Use txRef + logIndex for swap keys to avoid double-counting
  3. Skip special addresses: Exclude zero address and pool address from LP transfers
  4. Call reprice after LP changes: Ensures NAV is recalculated

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

Same events, different addresses?
  • Just update the pool addresses in your config
  • No code changes needed!
Different event signatures?
  • Update the ABI files
  • Regenerate TypeScript types
  • The rest stays the same
Different LP token logic?
  • Modify handleLpTransfer to match your protocol's behavior
  • Keep the same emit pattern