arctools for Arc

arc-index

One transfer. Two logs. Every indexer template gets this wrong.

Arc emits a Transfer log from a native system emitter for every USDC movement, and again from the ERC-20 contract when the movement went through the token interface. Both use the standard topic — so subscribing by topic double-counts.

Two emitters, one balance

SourceEmitterDecimalsCovers
Native system (EIP-7708) 0xffff…fffe 18 Every real movement
ERC-20 NativeFiatToken 0x3600…0000 6 ERC-20 interface calls only

Both use topic0 0xddf252ad…3b3ef. A plain native send emits one log; an ERC-20 transfer() emits two. Match on the emitter address, and never mix the 6-decimal and 18-decimal values.

What it costs to get wrong

Measured over 150 consecutive blocks of live Arc Testnet traffic.

  raw logs
    native emitter (18 dec)   1589
    ERC-20 emitter (6 dec)    1073

  canonical movements         1589
    initiated via ERC-20      1066  (logged twice by the chain)
    plain native sends        523   (logged once)
    sub-6-decimal dust        57    (invisible to balanceOf)

  ERC-20 logs the native emitter deliberately omits
    self-transfers            7     (from == to, no balance change)
    zero-value                0

  what a naive indexer gets wrong
    transfer rows             2662 vs 1589 real  (+67.5% phantom rows)
    volume, no dedup          11507.064102 USDC  (+83.1%)
    volume, canonical         6284.280334 USDC

  ✓ every value-moving ERC-20 log paired with its native log

The volume figure is the realistic failure: an indexer that normalises each emitter's decimals correctly but never deduplicates still overstates flow by 83%.

The exceptions that look like bugs

The native emitter records every movement — with two documented omissions, both of which the ERC-20 contract logs anyway because the token standard requires it:

Seven self-transfers appeared in that 150-block window. An indexer trusting the ERC-20 stream alone invents balance changes for every one of them. arc-index classifies them as expected rather than flagging them, and reserves anomalies for what should be impossible: a value-moving ERC-20 log with no native counterpart.

Proving an index, not trusting it

node src/cli.ts reconcile --address 0x… --blocks 60
  balance before   3065983.987369 USDC
  balance after    3065987.839446 USDC
  actual delta     3.852077 USDC   (eth_getBalance)
  indexed delta    3.852077 USDC   (6 movements)
  residual         0 USDC

  ✓ exact: every wei of movement is accounted for by logs

Replay every indexed movement for an address and compare against the node. Three outcomes, and the middle one matters:

The rule ships where you build indexers

The dedup logic is a pure function over normalised logs, with no RPC or framework dependency, so all three consumers agree on what a movement is.

Subsquid (SQD)

A batch processor is the natural fit: SQD hands you whole blocks, so both streams for a transaction are always in the same batch and the rule applies with no buffering.

const processor = new EvmBatchProcessor()
  .setRpcEndpoint(ARC_RPC_SETTINGS)
  .setFields({ log: REQUIRED_LOG_FIELDS })
  .addLog(arcUsdcLogRequest());          // one request, both emitters

processor.run(new TypeormDatabase(), async (ctx) => {
  const result = movementsFromBatch(ctx.blocks);
  await ctx.store.insert(toRows(result).map((r) => new UsdcMovement(r)));
});
SQD publishes no gateway for Arc

94 EVM networks are listed as of August 2026, none of them Arc. Ingestion is RPC-only — do not call setGateway, there is nothing to point it at.

Ponder

Ponder is event-at-a-time, so there is no batch in which to pair the two logs. The fix follows from what the chain guarantees:

  1. The native emitter logs every real movement — indexing it alone already yields each movement exactly once.
  2. The ERC-20 emitter adds no movements, only the knowledge that one was ERC-20-initiated.
  3. Arc emits the native log first, before any other log in the transaction.

So: native events create every row, and ERC-20 events update the row they duplicate. Point 3 is what makes that safe — the row always exists by the time the marker arrives, which makes double-counting structurally impossible rather than merely avoided.

Matching uses (txHash, from, to, value × 10¹²) ordered by logIndex, so two identical transfers in one transaction pair up in log order, exactly as the batch dedup does.

Operational limits worth knowing

Tests

29 tests across the core and both adapters, all against canned logs with no network: the pairing rule, one-to-one log consumption, dust truncation, the self-transfer and zero-value exceptions, and that net deltas sum to zero.