Skip to content

feat(abi): support ABI v2 structs and nested arrays#212

Open
0xbigapple wants to merge 23 commits into
tronprotocol:release_1.0.0from
0xbigapple:feature/support-dynamics-abi
Open

feat(abi): support ABI v2 structs and nested arrays#212
0xbigapple wants to merge 23 commits into
tronprotocol:release_1.0.0from
0xbigapple:feature/support-dynamics-abi

Conversation

@0xbigapple

@0xbigapple 0xbigapple commented Mar 26, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Extends ABI v2 (tuple/struct) support in the abi module across encoding and decoding:

  • Structs: encode/decode StaticStruct and DynamicStruct, including nested structs, arrays of structs, and nested array layouts represented through runtime TypeReference/innerTypes. Constructor-reflection decoding supports one array element-type level through the new @Parameterized annotation, which restores the erased element class for array-typed constructor parameters (the static-array size is derived from the concrete StaticArrayN class); nested array fields must use runtime type references.
  • Arrays: static arrays (StaticArrayN, plus the newly generated StaticArray0) and dynamic arrays with nested static/dynamic elements. Fixes missing offset pointers when a DynamicArray contains dynamic StaticArray elements.
  • Packed encoding: adds TypeEncoder.encodePacked and FunctionEncoder.encodeConstructorPacked for supported scalar and array shapes, with explicit rejection of directly supplied structs and declared nested-array component types.
  • Custom errors: new CustomError datatype and CustomErrorEncoder for encoding Solidity error definitions and computing their signature hashes.
  • Event signature compatibility: EventEncoder.encode / buildEventSignature keep their original behavior; new encodeWithOutPrefix / buildEventSignatureWithOutPrefix variants are added for callers that need the un-prefixed form.

Bug fixes and hardening included along the way:

  • Utf8String padded-length calculation now uses the UTF-8 byte length instead of the character count, so non-ASCII strings encode correctly; the arithmetic is overflow-checked and length-word semantics match web3j.
  • Struct signatures now serialize nested tuple types correctly, and unsupported nested-array shapes are rejected explicitly instead of producing wrong output.
  • Decode paths validate TypeReference nesting depth at the decoder entry. Reflective loading of externally supplied ABI type names avoids class initialization and validates that resolved classes implement the ABI Type interface.
  • Silent fallbacks are replaced with explicit errors, for example when building a Solidity type string for an empty array whose generic struct or nested-array element type cannot be determined, or when a struct field uses raw StaticArray instead of a generated StaticArrayN.

Why are these changes required?

Contracts compiled with pragma abicoder v2 (the default since Solidity 0.8) commonly use structs and nested arrays in function parameters, return values, and events. Trident already supported basic struct and array cases, but several ABI v2 combinations — such as runtime tuples, arrays of structs, nested array layouts, and array-typed struct fields — were incomplete or inconsistent across encoding, return decoding, and event-signature generation. This PR completes those paths.

This PR has been tested by:

  • Unit tests: extensive new coverage in abi — encoder/decoder tests for structs and nested arrays (DefaultFunctionEncoderTest, FunctionReturnDecoderTest, TypeDecoderTest, TypeEncoderTest), packed encoding (TypeEncoderPackedTest), constructor-reflection decoding (StructFieldReflectionDecodeTest, StaticArrayInStructDecodeTest), custom errors, and datatype-level tests (Utf8StringTest, DynamicArrayTest, StaticStructTest, etc.).
  • Compatibility tests: TridentAbiEncodeDecodeCompatibilityTest replays large fixture datasets (contract-interface*.json.gz) to verify encode/decode output stays consistent for real-world ABI inputs.
  • Nile integration test: AbiV2ContractTest in core calls a pre-deployed ABI v2 contract on Nile to verify static/dynamic structs, arrays of structs, multidimensional arrays, and packed encoding. The deployment case and the signed nested-struct transaction case remain disabled/manual.
  • Manual testing on Nile against the pre-deployed contract.

Follow up

Known limitations, deliberately kept aligned with the web3j implementation (documented in code comments) so they can be fixed upstream first and then synced back:

  • Constructor-reflection struct fields can express only one array element-type level; nested array fields require runtime TypeReference/innerTypes.
  • Packed array validation relies on the declared component type; an overly broad component type (e.g. Type.class) may admit unsupported runtime elements. Per-element validation is planned as a follow-up.
  • Address(Uint) is not normalized to 160 bits and can produce a non-canonical packed address; Address(BigInteger)/Address(String) produce the canonical 20 bytes.
  • Other web3j-inherited pitfalls are noted in code comments and left as-is to keep the diff against web3j minimal.

Extra details

  • Compatibility note: custom FunctionEncoder implementations must implement the new encodeWithSelector and encodePackedParameters abstract methods. Encoder/decoder providers are now resolved once during class initialization instead of per call, so providers registered afterwards are not picked up.
  • Existing EventEncoder entry points are unchanged; only new methods were added.
  • jackson-databind is added as a test-only dependency of the abi module (for reading JSON fixtures); no new runtime dependencies.
  • Legacy array constructors are marked @Deprecated with Javadoc pointing to the type-safe replacements.

…odeWithOutPrefix/buildEventSignatureWithOutPrefix
…ray elements

When encoding DynamicArray<StaticArray<DynamicBytes>> (e.g. bytes[3][]),the encoder was missing offset pointer table for inner elements.StaticArray with dynamic sub-elements was not recognized as needing offsets in encodeArrayValuesOffsets()
@0xbigapple 0xbigapple closed this Apr 24, 2026
  - Utils.getStructType: @parameterized on StaticArrayN struct fields now
    correctly emits "foo[N]" instead of collapsing to "foo[]". Throws
    RuntimeException on ClassNotFoundException instead of silent fallback;
    extractStaticArraySize now takes Class<?> and validates that the class
    is a generated StaticArrayN subclass (rejects raw StaticArray.class
    with a guidance message).

  - Utils.getParameterizedTypeName: build canonical ABI names for nested
    arrays via the new getArrayElementTypeName helper, which recurses on
    TypeReference rather than collapsing the inner type to a Class. The
    old path produced non-canonical strings like "staticarray2[]".

  - DefaultFunctionReturnDecoder.getDataOffset: include DynamicStruct in
    the dynamic-offset detection. The hand-rolled OR-chain was missing
    it, causing DynamicStruct return values to be decoded from the wrong
    offset.

  - DynamicArray.getTypeAsString: throw when an empty array's component
    type is the generic DynamicStruct/StaticStruct base or a raw Array
    subclass — Java type erasure cannot recover the inner element type
    in those cases. Concrete StructType subclasses still resolve through
    Utils.getStructType.

  - Utf8String.bytes32PaddedLength: guard against null value, matching
    the null-tolerance already in equals()/hashCode().

  - Array: remove the unused getNativeValueCopy() method. It was dead
    code in this PR (zero callers) and only unwrapped one level, so
    nested arrays/structs still contained ABI Type objects rather than
    native values.

  - UtilsTest: positive/negative cases for issue tronprotocol#20, DynamicArray
    regression guard, and canonical nested array type names
    (uint256[2][], uint256[2][2]).
  - EventEncoderTest: nested static array method signature.
  - DynamicArrayTest: empty concrete struct array (allowed),
    empty raw Array subclass (throws), empty generic struct array
    (throws).
  - Utf8StringTest: bytes32PaddedLength with null value.
  - CustomErrorTest: assert parameter count before per-element
    comparison; the previous loop could pass silently if getParameters()
    returned fewer elements than expected.
  - TridentAbiEncodeDecodeCompatibilityTest.isSignedIntOutOfRange:
    treat bare `int` as ABI alias for int256.
@0xbigapple 0xbigapple reopened this May 14, 2026
0xbigapple and others added 5 commits July 13, 2026 13:54
…plify DynamicStruct type-string branches
- decode trcToken[N] static arrays: instantiateStaticArray used the
  deprecated List-only StaticArrayN constructor, which re-derives the
  element type via AbiTypes.getType(getTypeAsString()); TrcToken's type
  string is "trcToken256" while its AbiTypes key is "trcToken", so
  decoding trcToken[N] failed on every path. Switch to the Class-carrying
  constructor and take the element type from the decoded elements.

- fail fast on truncated input in decodeNumeric: Arrays.copyOfRange
  silently zero-pads past the end of input, and the padding lands in the
  low-order bytes — a truncated Uint256 word decodes to
  value << (8 * missing bytes) with no error. Restore the fail-fast
  contract with a JDK 8-compatible explicit bounds check.

- keep T[0] on the static-array branch in instantiateArrayType: arraySize
  is -1 only for true dynamic-array references, but the dispatch tested
  <= 0, so uint256[0] silently instantiated as DynamicArray with type
  string "uint256[]" — a wrong function selector. With < 0, T[0]
  constructs a proper StaticArray0 and rejects non-empty values loudly.

- surface type-resolution failures in isDynamic(TypeReference): the
  method swallowed ClassNotFoundException/ClassCastException and answered
  "static", silently mis-placing every subsequent field's offset. Exclude
  StructType from the array branch explicitly (StaticStruct extends
  StaticArray), then rethrow resolution failures as
  UnsupportedOperationException.
Three defects in the constructor-reflection struct decoding path:

1. StaticArrayN struct fields were undecodable: they fell through to
   decode(String, int, Class), which rejects every Array subclass. New
   decodeStaticArrayStructField reads the element type from @parameterized
   (the same contract getStructType uses for signatures), builds the array
   reference, and decodes via decodeStaticArray. Static arrays of dynamic
   elements get a clear error pointing at innerTypes instead of garbage.

2. Nested-struct spans were guessed from reflection metadata —
   getDeclaredFields()[i]/getConstructors()[0] parameter counts in
   decodeStaticStructElement (wrong for non-public or extra java fields,
   arbitrary constructor pick, one-word-per-parameter assumption breaking
   doubly-nested structs), and the public-fields flat list in
   decodeDynamicStructElements (undercounts private fields). Both now
   advance by the decoded value's real span (bytes32PaddedLength), the
   pattern decodeArrayElements already used.

3. getTypeReferenceForParameterizedField round-tripped StaticArrayN element
   types through a lossy Solidity-name string (getSimpleTypeName lowercases,
   AbiTypes' lookup is case-sensitive), so TrcToken/Fixed/struct elements
   threw "Unsupported type encountered". It now builds the
   StaticArrayTypeReference directly from the classes at hand;
   getSimpleTypeName gains the camelCase trcToken token for signatures.
…lection path

string[2]-style struct fields are ABI-dynamic (head offset + tail) and already
encode correctly, but decodeDynamicStructElements misclassified them as static
and threw. Classify fields via isDynamicStructField (declared class OR
@parameterized element type) at all three decision points, and decode the tail
with the same machinery the innerTypes path uses. Add regression tests; assert
zero-length static array rejection at the construction layer per review.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ed nested arrays

Address three code-review findings on the runtime-tuple and nested-array paths:

- Utils.getTypeName: serialize runtime tuple TypeReferences (innerTypes form)
  as "(type1,type2,...)" so EventEncoder and CustomErrorEncoder can build
  signatures like E((uint256,string)) without a generated struct class.
  Previously this form hit constructor reflection on the DynamicStruct or
  StaticStruct base class and threw. Composes with arrays via
  getSubTypeReference, e.g. E((uint256,string)[]).

- Utils.getTypeReferenceForParameterizedField: fail fast when the
  @parameterized element type is itself a (non-struct) array class. The
  annotation carries a single element-type level, so nested array fields
  (e.g. uint256[2][2] as StaticArray2<StaticArray2<Uint256>>) previously
  produced invalid signatures like "(staticarray2[2])", threw
  ClassCastException, or silently misread tail data during decode.
  Documented as a known limitation of the constructor-reflection path;
  runtime innerTypes references (makeTypeReference) still support nesting.
  TypeDecoder.decodeDynamicParameterFromStruct now routes its DynamicArray
  branch through the same guard.

- TypeEncoder.isSupportingEncodedPacked: reject StaticArray component types.
  Solidity forbids nested arrays in abi.encodePacked; previously the encoder
  silently fell back to standard ABI encoding, emitting offset/length words
  that hash to wrong signatures. Checking the direct component type covers
  all nesting shapes since a nested array's component is itself an array.

Adds regression tests across EventEncoderTest, CustomErrorEncoderTest,
UtilsTest and TypeEncoderPackedTest.
- encodePacked: reject structs up front. DynamicStruct/StaticStruct
  extend the array classes and fell into arrayEncodePacked, silently
  producing corrupt packed bytes; Solidity's packed mode does not
  support structs.
- trcToken: emit the canonical camelCase "trcToken" from both
  AbiTypes.getTypeAString and instance-level TrcToken.getTypeAsString
  (previously "trctoken" / "trcToken256"), so selectors and the
  getType round-trip agree on every path; the override mirrors Address
  over its underlying Uint160.
- decoding: bound decode()'s numeric/address dispatch slices to one
  32-byte word and pass offsets instead of whole-tail substrings in the
  dynamic-struct paths. Decoding uint256[n] copied O(n^2) characters:
  uint256[16000] (1 MiB hex) took ~8.4s and now ~28ms. Chain responses
  are untrusted input, so this was a DoS amplification vector.
- StaticArray.getTypeAsString: handle empty struct components the way
  DynamicArray already does — tuple string via Utils.getStructType,
  descriptive error for generic struct / nested array / null component —
  instead of silently hashing lowercased Java class names into
  selectors.
- TypeReference depth validation: allow exactly MAX_TYPEREF_DEPTH
  nesting (off-by-one) and replace the iterative walker plus cycle set
  with bounded recursion; cycles necessarily trip the depth cap.
- consolidate StaticArrayN size parsing onto Utils.extractStaticArraySize,
  preferring StaticArrayTypeReference.getSize() where available;
  decoding uint256[33][2] now fails naming the missing generated
  StaticArray33 instead of a misleading zero-length-array error.
- TypeReference.getSubTypeReference: make the base declaration public
  (matching current upstream web3j) so subclasses outside the package
  can actually override it — package-private meant external
  same-signature methods only shadowed it, so wrapping a runtime-tuple
  innerTypes reference in a caller-built array reference decoded against
  a null subtype and threw.
- decode(String, int, TypeReference): every call throws.
  getRawType().getClass() always yields java.lang.Class, so both
  isAssignableFrom branches are unreachable. Inherited from web3j and
  never called inside the SDK; document the defect, the working
  alternatives (decodeStaticArray / decodeDynamicArray) and the intended
  fix instead of changing behavior.
- Address(Uint) packed width: encodePacked slices Address by the stored
  Uint's bit size, so only the 160-bit-normalizing constructors
  (Address(BigInteger), Address(String)) produce the canonical 20 packed
  bytes, while Address(Uint) packs to 32. Document the pitfall at the
  constructor and at the removePadding slice.
- route StaticArrayN size parsing through Utils.extractStaticArraySize
- collapse getDataOffset's dynamic check onto TypeDecoder.isDynamic
- state decodeNumeric's length check in word terms
- extract shared element-type-string logic into Array
- dedupe component-type inference in deprecated array constructors
- reuse TypeDecoder.isDynamic for TypeEncoder's empty static-array probe
- wrap two over-length lines in TypeDecoder.decode
… semantics

Return the length word plus the value padded to 32-byte words, matching
current web3j (null value still tolerated as empty). Port web3j's
Utf8String encoding and padded-length tests.
…ce); harden Utf8String padded length

- declare ClassNotFoundException on TypeDecoder.isDynamic(TypeReference) and drop
  the CNFE-to-UnsupportedOperationException wrapper so getDataOffset honors its
  checked throws contract; add regression tests covering unresolvable type
  references on both the getDataOffset and FunctionReturnDecoder.decode paths
- guard Utf8String.bytes32PaddedLength arithmetic with Math.addExact/multiplyExact
- re-enable testDecodeMultiDimDynamicArrayFunction, previously commented out when
  multi-dimensional dynamic arrays were unsupported
- test cleanups: reuse TypeEncoder.isDynamic in the compatibility test, drop a
  redundant anonymous DynamicArray subclass, remove unused imports, fix a javadoc
  typo, construct the test mapper via JsonMapper
@0xbigapple 0xbigapple changed the title feat(abi): support abiV2 include static/dynamics arrays, struct feat(abi): support ABI v2 structs and nested arrays Jul 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants