Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
4b6b23a
feat(abi): support abiV2 include static/dynamics arrays, struct
0xbigapple Mar 25, 2026
0b1c8e0
fix(abi): revert encode/buildEventSignature and add new function en…
0xbigapple Mar 25, 2026
a96b254
fix(abi): fix utf8String bytes32PaddedLength, fix Non-ASCII Charact…
0xbigapple Mar 26, 2026
83d18d3
fix: add offset pointers for DynamicArray containing dynamic StaticAr…
0xbigapple Apr 10, 2026
4a9bc2c
refactor(abi): unify static struct slot counting via bytes32PaddedLength
0xbigapple Apr 27, 2026
6951d9a
feat(abi): support StaticArray fields including dynamic-element variants
0xbigapple May 11, 2026
acfc4fd
fix(abi): throw explicit error when decoding empty array of generic s…
0xbigapple May 11, 2026
91ba349
test(abi): add ABI encode/decode compatibility tests with fixture data
0xbigapple May 11, 2026
b512792
security(abi): validate TypeReference depth at decoder entry
0xbigapple May 11, 2026
cfa0fee
security(abi): validate TypeReference depth at decoder entry
0xbigapple May 12, 2026
d2e4050
security(abi): centralize reflective type loading via safeLoadTypeCla…
0xbigapple May 12, 2026
8505480
fix(abi): fix reviewer feedback
0xbigapple May 14, 2026
7179b20
test(abi): clean up lint warnings in ABI test code
0xbigapple May 14, 2026
761cba2
docs(abi): add @deprecated Javadoc for legacy array constructors; sim…
0xbigapple Jul 13, 2026
11fc7ba
fix(abi): harden TypeDecoder edge cases in decoding paths
0xbigapple Jul 13, 2026
00cf825
fix(abi): repair struct constructor-reflection decoding in place
0xbigapple Jul 13, 2026
6fa1b4a
fix(abi): decode static arrays of dynamic elements on constructor-ref…
0xbigapple Jul 14, 2026
fd54a0f
fix(abi): serialize innerTypes tuples in signatures; reject unsupport…
0xbigapple Jul 14, 2026
93ef68d
fix(abi): harden encode/decode paths per ABI review
0xbigapple Jul 15, 2026
5833369
docs(abi): document pitfalls deliberately kept as-is
0xbigapple Jul 15, 2026
034b6e2
refactor(abi): consolidate decoder and array-type helper logic
0xbigapple Jul 16, 2026
13cabee
fix(abi): align Utf8String.bytes32PaddedLength with web3j length-word…
0xbigapple Jul 16, 2026
3918043
fix(abi): propagate ClassNotFoundException from isDynamic(TypeReferen…
0xbigapple Jul 17, 2026
a257c8a
fix(abi): harden signature building, packed gate, and TVM type semantics
0xbigapple Jul 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions abi/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ description 'TRON Application Binary Interface (ABI) for working with smart cont

dependencies {
implementation project(':utils')
testImplementation "com.fasterxml.jackson.core:jackson-databind:2.18.6"
}
43 changes: 43 additions & 0 deletions abi/src/main/java/org/tron/trident/abi/CustomErrorEncoder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package org.tron.trident.abi;

import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.stream.Collectors;
import org.tron.trident.abi.datatypes.CustomError;
import org.tron.trident.abi.datatypes.Type;
import org.tron.trident.crypto.Hash;
import org.tron.trident.utils.Numeric;

/**
* Ethereum custom error encoding. Further limited details are available <a
* href="https://docs.soliditylang.org/en/develop/abi-spec.html#errors">here</a>.
*/
public class CustomErrorEncoder {

private CustomErrorEncoder() {
}

public static String encode(CustomError error) {
return calculateSignatureHash(
buildErrorSignature(error.getName(), error.getParameters()));
}

static <T extends Type> String buildErrorSignature(
String errorName, List<TypeReference<T>> parameters) {

StringBuilder result = new StringBuilder();
result.append(errorName);
result.append("(");
String params =
parameters.stream().map(Utils::getTypeName).collect(Collectors.joining(","));
result.append(params);
result.append(")");
return result.toString();
}

public static String calculateSignatureHash(String errorSignature) {
byte[] input = errorSignature.getBytes(StandardCharsets.UTF_8);
byte[] hash = Hash.sha3(input);
return Numeric.toHexString(hash).substring(2);
}
}
47 changes: 45 additions & 2 deletions abi/src/main/java/org/tron/trident/abi/DefaultFunctionEncoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import java.util.List;
import org.tron.trident.abi.datatypes.Function;
import org.tron.trident.abi.datatypes.StaticArray;
import org.tron.trident.abi.datatypes.StaticStruct;
import org.tron.trident.abi.datatypes.Type;
import org.tron.trident.abi.datatypes.Uint;

Expand Down Expand Up @@ -64,11 +65,53 @@ private static String encodeParameters(
return result.toString();
}

/**
* Encodes a function call including its method signature (selector) and its parameters.
*
* @param methodId the 4-byte selector (8 hex chars)
* @param parameters the list of parameters to encode
* @return the complete ABI-encoded hex string representing the function call
*/
public String encodeWithSelector(String methodId, List<Type> parameters) {
final StringBuilder result = new StringBuilder(methodId);

return encodeParameters(parameters, result);
}

/**
* Encodes parameters using tight packing (abi.encodePacked).
* This is a non-standard ABI encoding used primarily for computing hashes,
* where padding is omitted and dynamic types are concatenated without length prefixes.
*
* @param parameters the list of parameters to pack
* @return the packed ABI-encoded hex string
*/
@Override
protected String encodePackedParameters(List<Type> parameters) {
final StringBuilder result = new StringBuilder();
for (Type parameter : parameters) {
result.append(TypeEncoder.encodePacked(parameter));
}
return result.toString();
}

/**
* Calculates the length of the tuple head (in 32-byte slots) required for the given parameters.
* Crucially, all dynamic types (including StaticArrays containing dynamic elements) always occupy exactly
* 1 slot in the head (for the offset pointer). Pure static arrays and structs are flattened to calculate
* their total inline slot requirement.
*
* @param parameters the list of types to be encoded.
* @return the total number of 32-byte slots required for the tuple head.
*/
@SuppressWarnings("unchecked")
private static int getLength(final List<Type> parameters) {
int count = 0;
for (final Type type : parameters) {
if (type instanceof StaticArray) {
count += ((StaticArray) type).getValue().size();
if (TypeEncoder.isDynamic(type)) {
count++;
} else if (type instanceof StaticArray || type instanceof StaticStruct) {
count += type.bytes32PaddedLength() / Type.MAX_BYTE_LENGTH;
} else {
count++;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@

package org.tron.trident.abi;

import static org.tron.trident.abi.TypeDecoder.MAX_BYTE_LENGTH_FOR_HEX_STRING;
import static org.tron.trident.abi.TypeDecoder.isDynamic;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.tron.trident.abi.datatypes.Array;
import org.tron.trident.abi.datatypes.Bytes;
import org.tron.trident.abi.datatypes.BytesType;
import org.tron.trident.abi.datatypes.DynamicArray;
import org.tron.trident.abi.datatypes.DynamicBytes;
import org.tron.trident.abi.datatypes.DynamicStruct;
import org.tron.trident.abi.datatypes.StaticArray;
import org.tron.trident.abi.datatypes.StaticStruct;
Expand All @@ -31,7 +33,7 @@
import org.tron.trident.utils.Strings;

/**
* Ethereum Contract Application Binary Interface (ABI) encoding for functions. Further details are
* Ethereum Contract Application Binary Interface (ABI) decoding for functions. Further details are
* available <a href="https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI">here</a>.
*/
public class DefaultFunctionReturnDecoder extends FunctionReturnDecoder {
Expand Down Expand Up @@ -73,60 +75,63 @@ public <T extends Type> Type decodeEventParameter(
}

private static List<Type> build(String input, List<TypeReference<Type>> outputParameters) {
// Reject cyclic or excessively nested TypeReference graphs up front; any
// downstream recursion through subTypeReference / innerTypes is then bounded
// by what passed validation here.
for (TypeReference<?> typeReference : outputParameters) {
Utils.validateTypeReferenceDepth(typeReference);
}

List<Type> results = new ArrayList<>(outputParameters.size());

int offset = 0;
for (TypeReference<?> typeReference : outputParameters) {
try {
int hexStringDataOffset = getDataOffset(input, offset, typeReference);

@SuppressWarnings("unchecked")
Class<Type> classType = (Class<Type>) typeReference.getClassType();

int hexStringDataOffset = getDataOffset(input, offset, classType);

Type result;
if (DynamicStruct.class.isAssignableFrom(classType)) {
if (outputParameters.size() != 1) {
throw new UnsupportedOperationException(
"Multiple return objects containing a struct is not supported");
}
result =
TypeDecoder.decodeDynamicStruct(
input, hexStringDataOffset, typeReference);
offset += TypeDecoder.MAX_BYTE_LENGTH_FOR_HEX_STRING;
TypeDecoder.decodeDynamicStruct(
input, hexStringDataOffset, typeReference);
offset += MAX_BYTE_LENGTH_FOR_HEX_STRING;

} else if (DynamicArray.class.isAssignableFrom(classType)) {
result =
TypeDecoder.decodeDynamicArray(
input, hexStringDataOffset, typeReference);
offset += TypeDecoder.MAX_BYTE_LENGTH_FOR_HEX_STRING;

} else if (typeReference instanceof TypeReference.StaticArrayTypeReference) {
int length = ((TypeReference.StaticArrayTypeReference) typeReference).getSize();
result =
TypeDecoder.decodeStaticArray(
input, hexStringDataOffset, typeReference, length);
offset += length * TypeDecoder.MAX_BYTE_LENGTH_FOR_HEX_STRING;
TypeDecoder.decodeDynamicArray(
input, hexStringDataOffset, typeReference);
offset += MAX_BYTE_LENGTH_FOR_HEX_STRING;

} else if (StaticStruct.class.isAssignableFrom(classType)) {
result =
TypeDecoder.decodeStaticStruct(
input, hexStringDataOffset, typeReference);
offset +=
classType.getDeclaredFields().length * TypeDecoder.MAX_BYTE_LENGTH_FOR_HEX_STRING;
TypeDecoder.decodeStaticStruct(
input, hexStringDataOffset, typeReference);
offset += (result.bytes32PaddedLength() / Type.MAX_BYTE_LENGTH)
* MAX_BYTE_LENGTH_FOR_HEX_STRING;

} else if (StaticArray.class.isAssignableFrom(classType)) {
int length =
Integer.parseInt(
classType
.getSimpleName()
.substring(StaticArray.class.getSimpleName().length()));
int length;
if (typeReference instanceof TypeReference.StaticArrayTypeReference) {
length = ((TypeReference.StaticArrayTypeReference) typeReference).getSize();
} else {
length = Utils.extractStaticArraySize(classType);
}
result =
TypeDecoder.decodeStaticArray(
input, hexStringDataOffset, typeReference, length);
offset += length * TypeDecoder.MAX_BYTE_LENGTH_FOR_HEX_STRING;

TypeDecoder.decodeStaticArray(
input, hexStringDataOffset, typeReference, length);
if (isDynamic(typeReference)) {
offset += MAX_BYTE_LENGTH_FOR_HEX_STRING;
} else {
offset +=
(result.bytes32PaddedLength() / Type.MAX_BYTE_LENGTH)
* MAX_BYTE_LENGTH_FOR_HEX_STRING;
}
} else {
result = TypeDecoder.decode(input, hexStringDataOffset, classType);
offset += TypeDecoder.MAX_BYTE_LENGTH_FOR_HEX_STRING;
offset += MAX_BYTE_LENGTH_FOR_HEX_STRING;
}
results.add(result);

Expand All @@ -137,10 +142,10 @@ private static List<Type> build(String input, List<TypeReference<Type>> outputPa
return results;
}

private static <T extends Type> int getDataOffset(String input, int offset, Class<T> type) {
if (DynamicBytes.class.isAssignableFrom(type)
|| Utf8String.class.isAssignableFrom(type)
|| DynamicArray.class.isAssignableFrom(type)) {
public static <T extends Type> int getDataOffset(
String input, int offset, TypeReference<?> typeReference)
throws ClassNotFoundException {
if (isDynamic(typeReference)) {
return TypeDecoder.decodeUintAsInt(input, offset) << 1;
} else {
return offset;
Expand Down
77 changes: 56 additions & 21 deletions abi/src/main/java/org/tron/trident/abi/FunctionEncoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@

package org.tron.trident.abi;

import static org.tron.trident.abi.TypeDecoder.instantiateType;
import static org.tron.trident.abi.TypeReference.makeTypeReference;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Iterator;
Expand All @@ -32,36 +35,55 @@
* @see DefaultFunctionEncoder
* @see FunctionEncoderProvider
*/

public abstract class FunctionEncoder {

private static FunctionEncoder DEFAULT_ENCODER;
private static final FunctionEncoder FUNCTION_ENCODER;

private static final ServiceLoader<FunctionEncoderProvider> loader =
ServiceLoader.load(FunctionEncoderProvider.class);
static {
ServiceLoader<FunctionEncoderProvider> loader =
ServiceLoader.load(FunctionEncoderProvider.class);
final Iterator<FunctionEncoderProvider> iterator = loader.iterator();

FUNCTION_ENCODER =
iterator.hasNext() ? iterator.next().get() : new DefaultFunctionEncoder();
}

public static String encode(final Function function) {
return encoder().encodeFunction(function);
return FUNCTION_ENCODER.encodeFunction(function);
}

/** Encode function when we know function method Id / Selector. */
public static String encode(final String methodId, final List<Type> parameters) {
return FUNCTION_ENCODER.encodeWithSelector(methodId, parameters);
}

public static String encodeConstructor(final List<Type> parameters) {
return encoder().encodeParameters(parameters);
return FUNCTION_ENCODER.encodeParameters(parameters);
}

public static String encodeConstructorPacked(final List<Type> parameters) {
return FUNCTION_ENCODER.encodePackedParameters(parameters);
}

public static Function makeFunction(
String fnname,
List<String> solidityInputTypes,
List<Object> arguments,
List<String> solidityOutputTypes)
throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
IllegalAccessException, InvocationTargetException {
throws ClassNotFoundException,
NoSuchMethodException,
InstantiationException,
IllegalAccessException,
InvocationTargetException {
List<Type> encodedInput = new ArrayList<>();
Iterator argit = arguments.iterator();
for (String st : solidityInputTypes) {
encodedInput.add(TypeDecoder.instantiateType(st, argit.next()));
encodedInput.add(instantiateType(st, argit.next()));
}
List<TypeReference<?>> encodedOutput = new ArrayList<>();
for (String st : solidityOutputTypes) {
encodedOutput.add(TypeReference.makeTypeReference(st));
encodedOutput.add(makeTypeReference(st));
}
return new Function(fnname, encodedInput, encodedOutput);
}
Expand All @@ -70,6 +92,31 @@ public static Function makeFunction(

protected abstract String encodeParameters(List<Type> parameters);

/**
* Encodes parameters prefixed with the given selector. Not part of the original
* subclass contract: implementations predating it inherit this throwing default,
* so the pre-existing entry points keep working and only the newer
* {@link #encode(String, List)} fails, with a clear message.
*
* @param methodId Callback selector / Abi method Id (Hex format)
*/
protected String encodeWithSelector(
final String methodId, final List<Type> parameters) {
throw new UnsupportedOperationException(
"encodeWithSelector is not implemented by " + getClass().getName()
+ "; override it to support FunctionEncoder.encode(methodId, parameters)");
}

/**
* Encodes parameters using tight packing (abi.encodePacked). Not part of the
* original subclass contract; see {@link #encodeWithSelector(String, List)}.
*/
protected String encodePackedParameters(List<Type> parameters) {
throw new UnsupportedOperationException(
"encodePackedParameters is not implemented by " + getClass().getName()
+ "; override it to support FunctionEncoder.encodeConstructorPacked(parameters)");
}

protected static String buildMethodSignature(
final String methodName, final List<Type> parameters) {

Expand All @@ -88,16 +135,4 @@ protected static String buildMethodId(final String methodSignature) {
final byte[] hash = Hash.sha3(input);
return Numeric.toHexString(hash).substring(2, 10);
}

private static FunctionEncoder encoder() {
final Iterator<FunctionEncoderProvider> iterator = loader.iterator();
return iterator.hasNext() ? iterator.next().get() : defaultEncoder();
}

private static FunctionEncoder defaultEncoder() {
if (DEFAULT_ENCODER == null) {
DEFAULT_ENCODER = new DefaultFunctionEncoder();
}
return DEFAULT_ENCODER;
}
}
Loading