feat(abi): support ABI v2 structs and nested arrays#212
Open
0xbigapple wants to merge 23 commits into
Open
Conversation
…odeWithOutPrefix/buildEventSignatureWithOutPrefix
…r Encoding, add unitest
…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()
- 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.
jj5419952-stack
approved these changes
Jun 7, 2026
…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.
jj5419952-stack
approved these changes
Jul 14, 2026
- 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Extends ABI v2 (
tuple/struct) support in theabimodule across encoding and decoding:StaticStructandDynamicStruct, including nested structs, arrays of structs, and nested array layouts represented through runtimeTypeReference/innerTypes. Constructor-reflection decoding supports one array element-type level through the new@Parameterizedannotation, which restores the erased element class for array-typed constructor parameters (the static-array size is derived from the concreteStaticArrayNclass); nested array fields must use runtime type references.StaticArrayN, plus the newly generatedStaticArray0) and dynamic arrays with nested static/dynamic elements. Fixes missing offset pointers when aDynamicArraycontains dynamicStaticArrayelements.TypeEncoder.encodePackedandFunctionEncoder.encodeConstructorPackedfor supported scalar and array shapes, with explicit rejection of directly supplied structs and declared nested-array component types.CustomErrordatatype andCustomErrorEncoderfor encoding Solidityerrordefinitions and computing their signature hashes.EventEncoder.encode/buildEventSignaturekeep their original behavior; newencodeWithOutPrefix/buildEventSignatureWithOutPrefixvariants are added for callers that need the un-prefixed form.Bug fixes and hardening included along the way:
Utf8Stringpadded-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.TypeReferencenesting depth at the decoder entry. Reflective loading of externally supplied ABI type names avoids class initialization and validates that resolved classes implement the ABITypeinterface.StaticArrayinstead of a generatedStaticArrayN.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:
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.).TridentAbiEncodeDecodeCompatibilityTestreplays large fixture datasets (contract-interface*.json.gz) to verify encode/decode output stays consistent for real-world ABI inputs.AbiV2ContractTestincorecalls 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.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:
TypeReference/innerTypes.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.Extra details
FunctionEncoderimplementations must implement the newencodeWithSelectorandencodePackedParametersabstract methods. Encoder/decoder providers are now resolved once during class initialization instead of per call, so providers registered afterwards are not picked up.EventEncoderentry points are unchanged; only new methods were added.jackson-databindis added as a test-only dependency of theabimodule (for reading JSON fixtures); no new runtime dependencies.@Deprecatedwith Javadoc pointing to the type-safe replacements.