Skip to content

Index Gnosis Safe Internal ETH Transfers via Input Data Decoding #44

@anhthii

Description

@anhthii

Bracnh with test case
(https://github.com/fystack/multichain-indexer/tree/indexing-gnosis-internal-tx) bracnh for the tests case continue from here

Problem

The EVM indexer cannot index ETH transfers made through Gnosis Safe (multisig) wallets. The outer execTransaction call has value=0 and the actual ETH moves as an internal transaction inside the Safe contract.

Example transaction: 0x7c98ff7c910b025736b11d2f70db001d5c2ec25df6de9fb65193963f6059b1f9

  • 0.1 ETH transferred from Safe (0x84ba2321d46814fb1aa69a7b71882efea50f700c) to 0xc26dC13d057824342D5480b153f288bd1C5e3e9d
  • Indexer sees value=0 on the outer tx and skips it entirely

Why Not debug_traceTransaction

  • Expensive and slow RPC call
  • Most providers don't support it by default or charge premium
  • Not scalable for high-throughput indexing

Proposed Approach

Decode the execTransaction input data directly + verify execution via receipt events. No new RPC methods needed.

How It Works

The Safe's execTransaction (method ID 0x6a761202) encodes transfer details in its input:

execTransaction(address to, uint256 value, bytes data, uint8 operation, ...)
Offset Parameter Description
0x00 to Transfer recipient
0x20 value ETH amount in wei
0x40 data Calldata (empty = pure ETH transfer)
0x60 operation 0 = Call, 1 = DelegateCall

Receipt events confirm whether execution succeeded:

  • ExecutionSuccess: 0x442e715f626346e8c54381002da614f62bee8d27386535b2521ec8540898556e
  • ExecutionFailure: 0x23428b18acfb3ea64b08dc0c1d296ea9c09702c09083ca5272e64d115b687d23

Detection Flow

Is input sig == 0x6a761202?
  +-- NO  --> existing logic (unchanged)
  +-- YES --> fetch receipt (batched with existing receipts)
              Has ExecutionSuccess event?
              +-- NO  --> skip (execution failed)
              +-- YES --> decode input params
                          operation == 0 (Call)?
                          +-- YES, data empty, value > 0  --> emit native_transfer
                          +-- YES, data non-empty          --> ERC20 logs handled by existing parseERC20Logs
                          +-- NO (DelegateCall)            --> skip (out of scope)

RPC Cost Impact

No new RPC methods or round-trips. The only change is a few extra tx hashes added to the existing BatchGetTransactionReceipts batch call.

Mode Extra RPC round-trips Extra receipts per block
With pubkeyStore (production) 0 ~0-2 (only for Safe txs targeting monitored addresses)
Without pubkeyStore 0 ~1-5 (batched with existing receipt fetches)

All input decoding and event checking is local byte parsing — no RPC calls.

Files to Change

1. internal/rpc/evm/tx.go — Constants

Add Safe-specific constants:

SAFE_EXEC_TRANSACTION_SIG    = "0x6a761202"
SAFE_EXECUTION_SUCCESS_TOPIC = "0x442e715f626346e8c54381002da614f62bee8d27386535b2521ec8540898556e"

2. internal/rpc/evm/tx.goNeedReceipt()

Add 0x6a761202 to the list of method sigs that require receipt fetching.

3. internal/rpc/evm/tx.go — New parseGnosisSafeTransfer() method

Decode execTransaction input to extract to, value, data, operation:

  • If operation == 0 (Call) AND data is empty AND value > 0native ETH transfer
    • FromAddress = Safe contract address (tx.To on the outer tx)
    • ToAddress = decoded to parameter
    • Amount = decoded value parameter
  • Verify receipt contains ExecutionSuccess event (not ExecutionFailure)

4. internal/rpc/evm/tx.goExtractTransfers()

Add Safe handling before existing native/ERC20 logic:

if input starts with 0x6a761202 && receipt has ExecutionSuccess:
    extract Safe internal transfer

5. internal/indexer/evm.goextractReceiptTxHashes()

When pubkeyStore != nil, also match Safe txs where the decoded recipient (to param from input) is a monitored address. This is pure byte parsing — no RPC cost.

6. internal/indexer/evm_test.go — Update test assertions

Change TestParseGnosisSafeETHTransfer to verify the transfer IS found:

  • Type: native_transfer
  • From: 0x84ba2321d46814fb1aa69a7b71882efea50f700c (Safe contract)
  • To: 0xc26dC13d057824342D5480b153f288bd1C5e3e9d
  • Amount: 100000000000000000 (0.1 ETH in wei)

Out of Scope (future work)

  • multiSend batched operations (0x8d80ff0a via DelegateCall) — requires decoding tightly-packed sub-transactions
  • DelegateCall operations (operation == 1) — different execution semantics
  • Other multisig contracts (non-Safe) — different ABIs

Acceptance Criteria

Automated Tests

  • TestParseGnosisSafeETHTransfer in internal/indexer/evm_test.go passes — verifies the Ethereum mainnet tx 0x7c98ff... is indexed as a native_transfer with:
    • From: 0x84ba2321d46814fb1aa69a7b71882efea50f700c (Safe contract)
    • To: 0xc26dC13d057824342D5480b153f288bd1C5e3e9d
    • Amount: 100000000000000000 (0.1 ETH)
  • go test ./internal/rpc/evm/ ./internal/indexer/ -v — all existing tests still pass (no regressions)
  • Unit test for parseGnosisSafeTransfer() covering:
    • Valid native ETH transfer (operation=0, empty data, value > 0)
    • Skipped when ExecutionFailure in receipt (not ExecutionSuccess)
    • Skipped when operation=1 (DelegateCall)
    • Skipped when value=0 (no ETH moved)

Manual Verification

Verify the indexer correctly picks up Safe execTransaction internal ETH transfers on the following networks by finding a real Safe tx on each block explorer (search for method ID 0x6a761202), running the indexer against that block, and confirming the transfer appears:

  • Ethereum Mainnet — using the reference tx above
  • Sepolia Testnet — find a Safe execTransaction with internal ETH transfer on sepolia.etherscan.io
  • Polygon Mainnet — find a Safe execTransaction with internal MATIC transfer on polygonscan.com
  • BSC Mainnet — find a Safe execTransaction with internal BNB transfer on bscscan.com

For each, confirm:

  1. Outer tx has value=0 and input starts with 0x6a761202
  2. Receipt contains ExecutionSuccess event
  3. Indexer emits a native_transfer with correct from (Safe contract), to, and amount

Non-Regression

  • Existing native ETH transfers (non-Safe) still indexed correctly
  • Existing ERC20 transfers still indexed correctly
  • No increase in RPC calls for blocks without Safe transactions
  • NeedReceipt() change does not cause receipt fetching for unrelated contract calls

Out of Scope (future work)

  • multiSend batched operations (0x8d80ff0a via DelegateCall) — requires decoding tightly-packed sub-transactions
  • DelegateCall operations (operation == 1) — different execution semantics
  • Other multisig contracts (non-Safe) — different ABIs

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions