ERC20 Holdings Adapter – Absinthe Docs
Skip to content

ERC20 Holdings Adapter

The simplest adapter — perfect starting point

What It Tracks

  • ERC20 token balances over time
  • Emits position windows for each holder

Design Reasoning

Why position not action?
Holdings are ongoing — a user holds tokens continuously, not just at a moment in time. Using position gives you time-weighted tracking.

Why token_based?
We're tracking fungible token amounts that can be priced.

Why no assetSelectors?
The contractAddress param uniquely identifies the token. There's only one asset per trackable instance.

Manifest

export const manifest = {
  name: 'erc20-holdings',
  version: '0.0.1',
  chainArch: 'evm',
  trackables: {
    token: {
      kind: 'position',            // Ongoing balances
      quantityType: 'token_based', // Fungible tokens
      params: {
        contractAddress: evmAddress('The contract address to track'),
      },
      // No assetSelectors needed — contractAddress is sufficient
    },
  },
} as const satisfies Manifest;

Event Handling

build: ({ config }) => {
  // Collect all token addresses we need to track
  const contractAddresses = new Set([
    ...config.token.map((item) => item.params.contractAddress)
  ]);
  
  // ERC20 Transfer event signature
  const transferTopic = erc20Abi.events.Transfer.topic;
 
  return {
    // Subscribe to Transfer events from our tokens
    buildSqdProcessor: (base) =>
      base.addLog({
        address: Array.from(contractAddresses),
        topic0: [transferTopic],
      }),
 
    // Process each Transfer event
    onLog: async ({ log, emitFns }) => {
      const { from, to, value } = erc20Abi.events.Transfer.decode(log);
      const assetAddress = log.address;
      
      // Find the matching config instance
      const instance = config.token?.find(
        (t) => t.params.contractAddress === assetAddress
      );
      if (!instance) return;
 
      // Decrease sender's balance
      await emitFns.position.balanceDelta({
        user: from,
        asset: { type: 'erc20', address: assetAddress },
        amount: -value,              // Negative = decrease
        activity: 'hold',
        trackableInstance: instance,
      });
      
      // Increase receiver's balance
      await emitFns.position.balanceDelta({
        user: to,
        asset: { type: 'erc20', address: assetAddress },
        amount: value,               // Positive = increase
        activity: 'hold',
        trackableInstance: instance,
      });
    },
  };
}

Example Config

{
  "chainArch": "evm",
  "flushInterval": "1h",
  "redisUrl": "${env:REDIS_URL}",
  "sinkConfig": {
    "sinks": [
      { "sinkType": "csv", "path": "positions.csv" },
      { "sinkType": "stdout" }
    ]
  },
  "network": {
    "chainId": 43111,
    "gatewayUrl": "https://v2.archive.subsquid.io/network/hemi-mainnet",
    "rpcUrl": "https://rpc.hemi.network/rpc",
    "finality": 75
  },
  "range": {
    "fromBlock": 2000000,
    "toBlock": 2005000
  },
  "adapterConfig": {
    "adapterId": "erc20-holdings",
    "config": {
      "token": [
        {
          "params": {
            "contractAddress": "0xAA40c0c7644e0b2B224509571e10ad20d9C4ef28"
          },
          "pricing": {
            "kind": "pegged",
            "usdPegValue": 121613.2
          }
        }
      ]
    }
  }
}

Config Fields

FieldDescription
params.contractAddressThe ERC20 token contract address
pricing.kindPricing strategy (pegged, coingecko, etc.)
pricing.usdPegValueFixed USD value (for pegged kind)

How to Modify for Your Protocol

Same events, different logic?
  • Change the filtering in onLog (e.g., exclude certain addresses)
Different token standard?
  • Use ERC721/ERC1155 ABIs instead
  • Update the asset type in balanceDelta
Track only certain holders?
  • Add a filters field to your trackable
  • Filter in onLog based on user addresses