Uniswap V2 Adapter
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)
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
| Trackable | Field | Description |
|---|---|---|
| swap | params.poolAddress | The UniV2 pool contract |
| swap | assetSelectors.swapLegAddress | Which token to price (token0 or token1) |
| lp | params.poolAddress | The UniV2 pool contract (= LP token) |
| lp | pricing.priceFeed.token0 | Pricing for underlying token0 |
| lp | pricing.priceFeed.token1 | Pricing for underlying token1 |
Key Patterns
- Cache token addresses: Token0/token1 never change, so cache in Redis
- Deduplication key: Use
txRef + logIndexfor swap keys to avoid double-counting - Skip special addresses: Exclude zero address and pool address from LP transfers
- 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!
- Update the ABI files
- Regenerate TypeScript types
- The rest stays the same
- Modify
handleLpTransferto match your protocol's behavior - Keep the same emit pattern