Skip to content

Instantly share code, notes, and snippets.

@jonaprieto
Created February 28, 2026 02:50
Show Gist options
  • Select an option

  • Save jonaprieto/0461747ee34d30e4cd696d11f7f8d73d to your computer and use it in GitHub Desktop.

Select an option

Save jonaprieto/0461747ee34d30e4cd696d11f7f8d73d to your computer and use it in GitHub Desktop.
Audit report for anoma-envio indexer (2026-02-27)

Audit Report: anoma-envio Indexer

Date: 2026-02-27 Scope: Full codebase — EventHandlers.ts, schema.graphql, decoders, config, tests Commit: a8e7aef (branch: next)

1. Architecture Overview

Envio HyperIndex indexer for the Anoma Protocol Adapter (PA-EVM) contracts. Indexes 11 on-chain events across 5 chains (Mainnet, Arbitrum, Base, Optimism, Sepolia) into a GraphQL API with 14 entity types. Core logic is ~960 lines in EventHandlers.ts plus ~440 lines of decoders.

Event processing order (within one EVM tx):

  1. Payload events (ResourcePayload, DiscoveryPayload, etc.)
  2. ForwarderCallExecuted
  3. CommitmentTreeRootAdded
  4. ActionExecuted (per action)
  5. TransactionExecuted (once, last)

2. Findings

CRITICAL — Action-to-Transaction assignment is wrong in multicall scenarios

Location: EventHandlers.ts:465-474

When multiple AP transactions share one EVM tx (multicall), pendingActionIds is keyed by evmTxId (shared by all AP txs). The TransactionExecuted handler does:

const actionIds = pendingActionIds.get(evmTxId) || [];
for (const actionId of actionIds) {
  const action = await context.Action.get(actionId);
  if (action) {
    context.Action.set({ ...action, transaction_id: txId });
  }
}
pendingActionIds.delete(evmTxId);

In a multicall with 2 AP transactions (TX-A and TX-B), the event sequence is:

  1. ActionExecuted for TX-A's actions → appended to pendingActionIds[evmTxId]
  2. ActionExecuted for TX-B's actions → appended to same pendingActionIds[evmTxId]
  3. TransactionExecuted for TX-A → assigns ALL pending actions (A's + B's) to TX-A, then deletes the key
  4. TransactionExecuted for TX-B → pendingActionIds.get(evmTxId) returns [], so TX-B gets no actions

Impact: In multicall scenarios, the first TransactionExecuted steals all actions from subsequent ones. Actions that belong to TX-B get incorrectly assigned to TX-A, and TX-B ends up with zero actions.

Root cause: The code assumes each TransactionExecuted only sees its own actions in the pending list, but since ActionExecuted events for all AP txs within the multicall fire before any TransactionExecuted, they all accumulate under the same evmTxId key.

Fix direction: Need a way to correlate which ActionExecuted events belong to which TransactionExecuted. Options include: (a) computing actionTreeRoot from decoded calldata to match, (b) using logIndex ranges, or (c) tracking a per-evmTx action counter and matching against decoded action count.


HIGH — Resource/Tag transaction_id never correctly assigned for multicall

Location: EventHandlers.ts:838

ResourcePayload creates Tags with transaction_id: evmTxId (temporary). The comment says "TransactionExecuted will set the proper txId." The TransactionExecuted handler does update transaction_id on existing tags (line 543), but there's no correlation logic to determine which TransactionExecuted a Tag belongs to when multiple AP txs share an EVM tx.

All tags from all AP txs in a multicall will get assigned to whichever TransactionExecuted processes them, with no way to distinguish ownership.


HIGH — Action matching heuristic is fragile

Location: EventHandlers.ts:625-653

The ActionExecuted handler tries to match the on-chain event to a decoded action from calldata:

for (let i = 0; i < decoded.actions.length; i++) {
  const potentialAction = decoded.actions[i];
  if (potentialAction.logicVerifierInputs.length === Number(event.params.actionTagCount)) {
    decodedAction = potentialAction;
    actionIndex = i;
    break;
  }
}

Problems:

  • Always picks the first match — if two actions have the same actionTagCount, the second ActionExecuted event will incorrectly match the same decoded action as the first.
  • No tracking of consumed matches — the same decoded action can be assigned to multiple ActionExecuted events.
  • Fallback to index 0 (line 650-653) — if no tag-count match is found, it silently uses decoded.actions[0], which may be completely wrong.

This means ComplianceUnit and LogicInput entities could contain data from the wrong decoded action.


MEDIUM — Stats race condition with unordered_multichain_mode

Location: EventHandlers.ts:274-294, config.yaml:60

With unordered_multichain_mode: true, handlers from different chains run concurrently. The stats pattern uses a global singleton (id: "global"):

const stats = await getOrCreateStats(context);
const updated = { ...stats, transactions: stats.transactions + 1, ... };
context.Stats.set(updated);

This is a classic read-modify-write race. If two handlers on different chains read Stats simultaneously, both see the same counter value, both increment by 1, and one write overwrites the other — losing a count. DailyStats has the same issue when events from different chains land in the same UTC day.

Note: Whether this actually manifests depends on Envio's internal concurrency model. If context.Stats.get + context.Stats.set is serialized per entity ID within the runtime, this is safe. But the unordered_multichain_mode flag suggests otherwise.


MEDIUM — EVMTransaction entity silently overwritten in multicall

Location: EventHandlers.ts:436-449

Every TransactionExecuted in the same EVM tx creates/overwrites the same EVMTransaction entity (same evmTxId). The fields are identical so no data loss occurs today, but if gas-related fields ever differ between calls (e.g., after an EIP change), the last write wins silently.


MEDIUM — pendingActionIds cache eviction can lose data

Location: EventHandlers.ts:214

pendingActionIds uses BoundedCache with max size 1000. If >1000 distinct EVM transactions are being processed concurrently (before their TransactionExecuted fires), older entries get evicted via FIFO. Those evicted actions will never have their transaction_id corrected.

With unordered_multichain_mode: true across 5 chains, high-throughput periods could hit this limit. The decodedCalldataCache has the same risk — if evicted before TransactionExecuted runs, the calldata won't be available for proof extraction.


MEDIUM — Tag deduplication issue across AP transactions

Location: EventHandlers.ts:534-565

If the same tag hash appears in two different AP transactions (which is valid — one creates a commitment, a later tx consumes it as a nullifier), createTagId produces the same ID: {chainId}_{tagHash}. The second TransactionExecuted will overwrite the first Tag entity's isConsumed, index, and transaction_id.

This means a tag that was originally isConsumed: false (created) will get overwritten to isConsumed: true (consumed) when it later appears as a nullifier, losing the creation record.


LOW — start_block: 0 on all networks

Location: config.yaml:31,38,44,50,55

All networks start indexing from block 0. The PA-EVM contracts were deployed much later. This wastes sync time scanning millions of empty blocks. Should be set to the contract deployment block for each chain.


LOW — No handler for OwnershipTransferred / Paused / Unpaused

Location: config.yaml:12-14

These events are declared in config but have no handlers. They'll be received and silently dropped. Not a bug, but wasted bandwidth from HyperSync.


LOW — LogicRef.id is the raw logicRef hex without chainId

Location: EventHandlers.ts:572-585

LogicRef entities use the raw hex value as ID without chainId prefix. If the same logicRef appears on two different chains, firstSeen* fields will record whichever chain processes it first, and the second chain's record is silently lost.


LOW — Compliance unit tag linking may fail silently

Location: EventHandlers.ts:686-687

If the Tag entity hasn't been created yet (possible if ResourcePayload hasn't fired or processed before ActionExecuted), the compliance unit will have consumedTag_id: undefined and createdTag_id: undefined. Same applies at line 736 for LogicInput → Tag linking.

Since the event ordering doc says payloads fire before ActionExecuted, this should usually work — but unordered_multichain_mode or indexer restarts could break the assumption.


INFO — Missing gas/gasPrice in field_selection

Location: config.yaml:18-26, EventHandlers.ts:422-424

The handler reads tx.gas and tx.gasPrice, but field_selection only includes gasUsed, not gas or gasPrice. These fields will be undefined in the EVMTransaction entity.


INFO — consumedCount math assumes even tag count

Location: EventHandlers.ts:589-590

const consumedCount = Math.floor(totalTags / 2);
const createdCount = totalTags - consumedCount;

If totalTags is odd (a contract-level invariant violation), the stats would silently miscount. Consider logging a warning for odd tag counts.


3. Test Coverage

Area Coverage Notes
CommitmentTreeRoot ordering Good 3 scenarios for logIndex
Transaction ID collision Good multicall scenario covered
Stats counters Good isolation and accumulation
DailyStats counters Good bucketing and isolation
getUTCDay Good boundaries and sortability
ActionDecoder Good real mainnet calldata fixture
ResourceDecoder Good format detection edge cases
Action matching heuristic Missing No test for multiple actions with same tagCount
Multicall action assignment Missing No test combining ActionExecuted + TransactionExecuted in multicall
Cross-chain stats race Missing No concurrency test
Tag overwrite on re-use Missing No test for create→consume lifecycle

4. Summary

Severity Count Key Theme
Critical 1 Action-to-Transaction assignment broken in multicall
High 2 Tag transaction_id wrong in multicall; action matching heuristic
Medium 4 Stats race condition; EVMTransaction overwrite; cache eviction; tag dedup
Low 4 start_block, unhandled events, LogicRef dedup, tag linking timing
Info 2 Missing field_selection fields; consumedCount math

The single-AP-tx-per-EVM-tx path works correctly. The critical issues all emerge in the multicall path (multiple execute() calls in one EVM transaction), which was the exact scenario PR #26 aimed to fix — the Transaction ID collision is fixed, but the cascading effects on Action assignment and Tag ownership were not addressed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment