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
Dependencies
References
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
EvmTokenTransferdomain object. It is used byEvmTransaction.extractTokenTransfers()to project token transfer data from transaction logs.Specification
File
prism/src/main/java/com/stablebridge/prism/domain/service/Erc20TransferDecoder.javaImplementation
Key decoding details
substring(26)skips "0x" (2 chars) + 24 zero chars = 26 chars, yielding the 40-char address.substring(2)strips the "0x" prefix.addressfield of the log entry identifies which ERC-20 contract emitted the event.Design decisions
final classwith private constructor — stateless utilityOptional.empty()for non-ERC-20 logs rather than throwingErc20Constants.isErc20Transfer()(single responsibility)BigIntegerfor raw amount (token decimals are token-specific, not applied here)Test class
prism/src/test/java/com/stablebridge/prism/domain/service/Erc20TransferDecoderTest.javaTest cases:
EvmTokenTransferwith from, to, amount, token addressAcceptance Criteria
Erc20TransferDecoderclass exists at the specified pathdecode()correctly extracts from, to, amount, and token address from valid ERC-20 logsOptional.empty()(no exceptions)BigInteger./gradlew buildpassesDependencies
isErc20Transfer()checkEvmEventLogandEvmTokenTransfertypesReferences