ERC20 Holdings Adapter
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
| Field | Description |
|---|---|
params.contractAddress | The ERC20 token contract address |
pricing.kind | Pricing strategy (pegged, coingecko, etc.) |
pricing.usdPegValue | Fixed 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)
- Use ERC721/ERC1155 ABIs instead
- Update the asset type in
balanceDelta
- Add a
filtersfield to your trackable - Filter in
onLogbased on user addresses