Skip to main content

Technical Details

This page covers technical specifications for working with ERC token data on Hedera, including account identifiers, event signatures, and data formats.

Data Pipeline Overview​

The Hedera ERC Indexer processes token data through a seven-stage pipeline:

Key Pipeline Stages​

  1. Discovery: Scans Hedera mirror node logs for contracts emitting Transfer, ERC-1155 TransferSingle/TransferBatch, or ERC-1400 partition events
  2. Classification and Extraction: Probes each contract via JSON-RPC to determine its standard, then retrieves its metadata (name, symbol, decimals, total supply)
  3. Balance Fetching: Fetches current balances via RPC balanceOf() calls
  4. Transfer History: Extracts and indexes transfer events to tables
  5. NFT Tracking: For ERC-721 tokens, tracks individual NFT ownership and metadata
  6. Accrual Refresh: Re-reads balances of interest-bearing tokens on a timer (every 15 minutes)
  7. Storage: Persists data to erc schema tables exposed via Hasura GraphQL

Account Identifiers and Aliases​

Understanding Hedera Account Systems​

Every entity on Hedera has a unique account ID (e.g., 0.0.12345), but accounts can be referenced through multiple identifiers:

1. Account Number (Native Hedera Format)​

  • Format: <shard>.<realm>.<account_number> (e.g., 0.0.924713)
  • Usage: Native Hedera transactions and queries
  • In Topics: Stored as 1-8 bytes depending on size

2. Account-num Alias (Long-zero Address)​

  • Format: 20-byte EVM address with 12 leading zeros
  • Example: 0x00000000000000000000000000000e1c29 (Account 924713)
  • Purpose: Makes every Hedera account EVM-compatible
  • Resolution: Direct calculation from bytes (no lookup needed)

3. ECDSA EVM Alias​

  • Format: Standard 20-byte Ethereum address
  • Example: 0x6d9f1a927cbcb5e2c28d13ca735bc6d6131406da
  • Purpose: Allows MetaMask and EVM wallets to interact with Hedera
  • Resolution: Requires lookup to map to Hedera account ID

Hollow Accounts and Auto-Creation​

When an EVM address receives tokens or HBAR before having a Hedera account:

  1. Hollow Account Creation: System automatically creates an incomplete account
  2. Properties: Has account ID and alias, but no key (can receive but not send)
  3. Completion: Becomes full account when used as fee payer with proper signature
  4. Important: Every hollow account still has a unique Hedera ID

Only HBAR and HTS transfers trigger this. A pure ERC token balance is an entry in the token contract's own storage, so an address can hold ERC tokens with no Hedera account at all. The indexer keeps those holders: in the balance tables they have a NULL account_id and their address in holder_evm_address. If the address is later activated, the token's next balance update moves its row to the new account ID.

Event Detection & Signatures​

Transfer Event​

All ERC-20 and ERC-721 tokens emit a standardized Transfer event:

event Transfer(address indexed from, address indexed to, uint256 value_or_tokenId)

Event Signature Hash:

0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef

This signature is the keccak256 hash of the event declaration and is always the same for all ERC Transfer events.

Approval Event​

event Approval(address indexed owner, address indexed spender, uint256 value)

Event Signature Hash:

0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925

ERC-1400 Events​

Security tokens use partition-based events for transfers:

EventSignature Hash
IssuedByPartition0x5af1c8f424b104b6ba4e3c0885f2ed9fef04a9b1ea39cd9ed362432105c0791a
TransferByPartition0xff4e9a26af4eb73b8bacfaa4abd4fea03d9448e7b912dc5ff4019048875aa2d4
RedeemedByPartition0xa4f62471c9bdf88115b97203943c74c59b655913ee5ee592706d84ef53fb6be2

ERC-1155 Events​

EventSignature Hash
TransferSingle0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62
TransferBatch0x4a39dc06d4c0dbc64b70af90fd698a233a518aa5d07e595d983b8c0526c8f7fb

Token Type Detection​

The indexer classifies each contract by calling its methods over JSON-RPC, and falls back to the shape of its Transfer events only when the RPC gives no evidence either way:

  1. totalSupply() decides the path. A contract without it is checked for ERC-721 with ownerOf(0); one with it continues below
  2. ERC-1400: isIssuable() and isControllable() both return true
  3. ERC-1155 or ERC-721: ERC-165 supportsInterface() declares the interface
  4. ERC-3643: identityRegistry() and compliance() both return non-zero addresses
  5. ERC-20 or ERC-721 by method probe: decimals() versus ownerOf() / tokenURI()
  6. Fallback: the Transfer events' topic3, described below

If the RPC endpoint fails to answer, the contract is left unclassified and retried later rather than guessed. Contracts discovered through TransferSingle/TransferBatch events are ERC-1155.

ERC-20 Detection (Event Fallback)​

  • Characteristic: topic3 IS NULL
  • Reason: Transfer value is stored in the event's data section
  • Structure: Transfer(from, to, amount) - amount goes in data section

ERC-721 Detection (Event Fallback)​

  • Characteristic: topic3 IS NOT NULL
  • Reason: Unique tokenId is indexed and gets its own topic slot
  • Structure: Transfer(from, to, tokenId) - tokenId becomes topic3

Contracts That Are Not Tokens​

Some contracts emit the ERC-20 Transfer signature without being tokens: for example, contracts whose receive() logs Transfer(sender, self, msg.value) whenever they are sent HBAR. A contract whose every Transfer is addressed to itself is not indexed as a token.

ERC-1155 Detection​

  • Method: ERC-165 supportsInterface(0xd9b67a26), or discovery through TransferSingle/TransferBatch events

ERC-3643 Detection​

  • Method: RPC probing via identityRegistry() (0x134e18f4) and compliance() (0x6290865d)
  • Characteristic: Both return non-zero addresses

ERC-1400 Detection​

  • Method: RPC probing via isIssuable() and isControllable()
  • Selectors: 0x2f1cae85 (isIssuable), 0x4c783bf5 (isControllable)
  • Characteristic: Both methods must return true for positive detection
  • Note: ERC-165 supportsInterface is NOT used (returns false for valid ERC-1400 tokens)

Standard ERC Methods​

ERC tokens implement standard methods that the indexer queries:

Common Methods (All Standards)​

function name() external view returns (string memory);
function symbol() external view returns (string memory);

ERC-20 Specific Methods​

function decimals() external view returns (uint8);
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);

ERC-721 Specific Methods​

function totalSupply() external view returns (uint256); // Optional
function ownerOf(uint256 tokenId) external view returns (address);
function tokenURI(uint256 tokenId) external view returns (string memory);

ERC-1155 Specific Methods​

function balanceOf(address account, uint256 id) external view returns (uint256);

name() and symbol() are optional for ERC-1155.

Reliability Scoring​

The indexer calculates quality scores based on metadata completeness to help identify well-formed contracts.

Scoring Methodology​

The metadata_reliability_score field indicates how many standard methods returned valid data:

ERC-20 Tokens (4 fields)​

Fields checked: name, symbol, decimals, totalSupply

ScoreFields ExtractedMeaning
1.004 of 4Fully compliant ERC-20
0.753 of 4Mostly compliant
0.502 of 4Partial implementation
0.251 of 4Minimal implementation
0.000 of 4No metadata available

ERC-721 NFTs (3 fields)​

Fields checked: name, symbol, totalSupply (decimals not applicable)

ScoreFields ExtractedMeaning
1.003 of 3Fully compliant ERC-721
0.672 of 3Mostly compliant
0.331 of 3Minimal implementation
0.000 of 3No metadata available

ERC-1400 Security Tokens (4 fields)​

Fields checked: name, symbol, decimals, totalSupply (same as ERC-20)

ScoreFields ExtractedMeaning
1.004 of 4Fully compliant ERC-1400
0.753 of 4Mostly compliant
0.502 of 4Partial implementation
0.251 of 4Minimal implementation
0.000 of 4No metadata available

ERC-3643 Tokens (4 fields)​

Fields checked: name, symbol, decimals, totalSupply, scored like ERC-20.

ERC-1155 Contracts (2 fields)​

Fields checked: name, symbol, which the standard leaves optional. A contract that implements neither scores 0.50 rather than 0.00.

ScoreFields ExtractedMeaning
1.002 of 2Both optional fields present
0.501 of 2, or noneOptional fields missing

Unknown/Failed Deployments​

For contracts that couldn't be identified as one of the standards above:

ScoreFields ExtractedMeaning
1.00name AND symbolBasic token interface
0.50name OR symbolPartial interface
0.00NeitherNo standard interface

Using Reliability Scores​

Filter for high-quality tokens in your queries:

query ReliableTokens {
erc_token(
where: {
metadata_reliability_score: { _gte: 0.75 }
contract_type: { _in: ["ERC_20", "ERC_721"] }
}
) {
name
symbol
metadata_reliability_score
}
}

Data Encoding Formats​

Address Encoding​

Addresses in the GraphQL API are always returned as strings in hex format with 0x prefix:

  • EVM Addresses: 0x + 40 hex characters
  • Example: 0x00000000000000000000000000000000002cc823

Numeric Values​

Large numbers (balances, token supplies) are returned as strings to preserve precision:

  • Format: Decimal string representation
  • Example: "1000000000000000000" (1 token with 18 decimals)
  • Max Value: Full uint256 range (2^256 - 1)

Timestamps​

All timestamps use nanoseconds since epoch (Hedera format):

Field TypeExampleDescription
created_timestamp1698765432100000000Entity creation time
balance_timestamp1698765432100000000Last balance update
processing_timestamp1698765432100000000Record processing time
transfers_indexed_timestamp1698765432100000000Last transfer indexed
consensus_timestamp1698765432100000000Transaction time

Working with Token Balances​

ERC-20 Balances​

Balances are stored in the smallest unit (wei):

// Convert balance to human-readable format
function formatBalance(balance, decimals) {
return (BigInt(balance) / BigInt(10 ** decimals)).toString();
}

// Example: 1000000000000000000 with 18 decimals = 1 token

ERC-721 Balances​

For NFTs, the balance represents the count of NFTs owned:

query NFTCount($accountId: bigint!, $tokenId: bigint!) {
erc_token_account(where: { account_id: { _eq: $accountId }, token_id: { _eq: $tokenId } }) {
balance # This is the NFT count, not wei
}
}

Balance Fetching​

Balances are fetched via RPC balanceOf() calls rather than calculated from Transfer events. This ensures accuracy for:

  • Standard ERC-20: Exact match with on-chain state
  • Rebasing tokens (aTokens) and debt tokens: their balances grow without Transfer events. The indexer flags them (balance_accrues = true, detected by scaledTotalSupply()) and re-reads their balances every 15 minutes, so they trail the chain by at most about that long
  • ERC-1400: erc_token_account.balance is the transferable balance from balanceOf()
  • ERC-1155: balanceOf(address, id) for each holder and token type, stored in erc_multi_token_account
note

ERC-1400 per-partition balances are computed from the partition events and stored in erc_token_partition_balance. For tokens that keep balances only per partition (where balanceOf() returns 0), that table still shows each holder's position.

Process​

  1. Account Discovery: Extract unique addresses from Transfer event topics
  2. Address Resolution: Resolve EVM addresses to Hedera account IDs (two-pass strategy)
  3. RPC Batch Calls: Fetch current balances via balanceOf(address) method
  4. Storage: Persist balances to erc_token_account table

Why RPC Instead of Event Aggregation?​

Event-based balance calculation (summing Transfer amounts) fails for:

  • Rebasing tokens: Balance changes without Transfer events (e.g., Aave aTokens accrue interest continuously)
  • Debt tokens: Interest accrues without events
  • Fee-on-transfer tokens: Actual received amount differs from event amount

RPC balanceOf() queries the contract's current state directly, providing exact balances for all token types.