Skip to content

EVM-15: Erc20TransferDecoder domain service #153

@Puneethkumarck

Description

@Puneethkumarck

Context

ERC-20 token transfers are encoded in Ethereum event logs with a specific structure: topic0 is the Transfer event signature hash, topic1 is the sender (zero-padded to 32 bytes), topic2 is the recipient (zero-padded to 32 bytes), and the data field contains the transfer amount as a uint256. This decoder extracts these fields from raw log data and produces an EvmTokenTransfer domain object. It is used by EvmTransaction.extractTokenTransfers() to project token transfer data from transaction logs.

Specification

File

prism/src/main/java/com/stablebridge/prism/domain/service/Erc20TransferDecoder.java

Implementation

public final class Erc20TransferDecoder {
    private Erc20TransferDecoder() {}

    /**
     * Decode an ERC-20 Transfer event from a log entry.
     * Returns empty if the log is not a valid ERC-20 Transfer.
     */
    public static Optional<EvmTokenTransfer> decode(
            TransactionHash txHash,
            long blockNumber,
            EvmEventLog log) {

        // Step 1: Check if this is an ERC-20 Transfer via topic count and topic0
        if (!Erc20Constants.isErc20Transfer(log.topics())) {
            return Optional.empty();
        }

        // Step 2: Validate data field is present and sufficient
        var data = log.data();
        if (data == null || data.length() < 3) {  // at minimum "0x" + 1 digit
            return Optional.empty();
        }

        // Step 3: Extract 'from' address — topic[1], last 40 hex chars (20 bytes)
        var fromHex = "0x" + log.topics().get(1).substring(26);
        var from = new EvmAddress(fromHex);

        // Step 4: Extract 'to' address — topic[2], last 40 hex chars (20 bytes)
        var toHex = "0x" + log.topics().get(2).substring(26);
        var to = new EvmAddress(toHex);

        // Step 5: Extract transfer amount from data field (uint256)
        var rawAmount = new BigInteger(data.substring(2), 16);

        // Step 6: Token contract address is the log emitter
        var tokenAddress = log.address();

        return Optional.of(EvmTokenTransfer.builder()
            .txHash(txHash)
            .chainId(0)  // caller should set chainId
            .blockNumber(blockNumber)
            .logIndex(log.logIndex())
            .tokenAddress(tokenAddress)
            .from(from)
            .to(to)
            .rawAmount(rawAmount)
            .build());
    }
}

Key decoding details

  • Topic padding: EVM topics are 32 bytes (64 hex chars + "0x"). Addresses are 20 bytes, so they are left-padded with 12 bytes of zeros. substring(26) skips "0x" (2 chars) + 24 zero chars = 26 chars, yielding the 40-char address.
  • Data field: Contains the uint256 transfer amount as a hex string. substring(2) strips the "0x" prefix.
  • Token address: The address field of the log entry identifies which ERC-20 contract emitted the event.
  • Chain ID: Set to 0 as a placeholder — the caller (EvmTransaction) should set the correct chain ID.

Design decisions

  • final class with private constructor — stateless utility
  • Returns Optional.empty() for non-ERC-20 logs rather than throwing
  • Delegates ERC-20 detection to Erc20Constants.isErc20Transfer() (single responsibility)
  • Uses BigInteger for raw amount (token decimals are token-specific, not applied here)
  • No framework imports

Test class

prism/src/test/java/com/stablebridge/prism/domain/service/Erc20TransferDecoderTest.java

Test cases:

  • Valid ERC-20 Transfer log decodes to correct EvmTokenTransfer with from, to, amount, token address
  • Log with wrong topic0 returns empty
  • Log with 4 topics (ERC-721) returns empty
  • Log with 2 topics returns empty
  • Log with null data returns empty
  • Log with empty data ("0x") returns empty
  • Zero-padded addresses are correctly extracted (last 40 hex chars)
  • Large uint256 amounts decode correctly via BigInteger

Acceptance Criteria

  • Erc20TransferDecoder class exists at the specified path
  • decode() correctly extracts from, to, amount, and token address from valid ERC-20 logs
  • Non-ERC-20 logs return Optional.empty() (no exceptions)
  • Address extraction handles 32-byte zero-padded topics correctly
  • Large token amounts are handled via BigInteger
  • Zero framework imports in domain layer
  • All test cases pass
  • ./gradlew build passes

Dependencies

References

Metadata

Metadata

Labels

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions