fix(clear_signing): parse arrays of static structs per the ABI spec.
What changed, and why it matters
This commit fixes a bug in how Trezor firmware parses Ethereum transaction data for 'clear signing' of complex smart-contract calls. The parser previously misread arrays of simple structs (e.g., lists of token-swap details) because it assumed every struct element was stored via an offset pointer, which is only true for structs containing variable-length data. With the correct ABI behavior, static structs are now read directly in sequence. The bug could cause Trezor to display wrong values or fall back to blind signing, but it does not leak private keys or directly steal funds. The fix also hardens parsing against malformed empty structs and out-of-bounds reads.
Treat this as a security-hardening correctness fix for the Ethereum clear-signing parser. Reviewers should verify that the new canonical encoding matches the Solidity ABI spec for arrays of static structs, confirm the updated test vectors and signatures are valid, and ensure the empty-tuple rejection and bounds checks are present in shipped firmware. Consider whether prior firmware versions could be induced to blind-sign or mis-display crafted calldata and issue an advisory if user-visible risk is confirmed.
Security signals we found
Incorrect ABI decoding of arrays of static structs could lead to mis-displayed transaction fields during clear signing.
Mis-decoded fields may cause users to approve transactions different from what they see, or trigger blind-signing fallback.
Bounds-check improvements and rejection of zero-field tuples reduce denial-of-service / unbounded parse risks.
No evidence of private-key exposure, arbitrary code execution, or direct fund theft in the diff.
Evidence from the diff
The patch rewrites ABI parsing in core/src/apps/ethereum/clear_signing.py so that ABIValue.parse() handles the head (offset vs. in-place) generically, and subclasses only implement parse_body(). Previously, Array unconditionally treated struct elements as dynamic (offset-headed), and from_proto lied about is_dynamic for array-nested tuples to make that work. That happened to handle dynamic structs (e.g., LiFi callData with bytes) but broke arrays of fully static structs, whose elements are encoded contiguously with no offsets. The fix makes is_dynamic the truthful type-level property, derives it from fields for array-nested tuples, and updates test fixtures and vectors accordingly. It also rejects empty Tuples to prevent a zero-stride array loop and tightens bounds checks.
Changed components
core/src/apps/ethereum/clear_signing.pycommon/protob/messages-definitions.protocommon/tests/fixtures/ethereum/sign_tx_clear_signing.jsoncore/tests/test_apps.ethereum.clear_signing.pytests/device_tests/ethereum/test_definitions_request.pyInspect captured patch +186 / −144
diff --git a/common/protob/messages-definitions.proto b/common/protob/messages-definitions.proto
index 273027e8..a5791718 100644
--- a/common/protob/messages-definitions.proto
+++ b/common/protob/messages-definitions.proto
@@ -108,6 +108,9 @@ enum EthereumABIType {
*/
message EthereumABITupleInfo {
repeated EthereumABIValueInfo fields = 1;
+ // Whether the tuple is a dynamic ABI type, i.e. at least one of its
+ // fields is dynamic. For tuples nested inside an array the firmware
+ // ignores this flag and derives the value from `fields` instead.
required bool is_dynamic = 2;
}
@@ -118,7 +121,7 @@ message EthereumABITupleInfo {
message EthereumABIValueInfo {
// Exactly one of the following should be set:
optional EthereumABIType atomic = 1; // Atomic(parser)
- optional EthereumABIType dynamic = 2; // Dynamic(parser)
+ optional EthereumABIType dynamic = 2; // DynamicLeaf(parser)
optional EthereumABITupleInfo tuple = 3; // Tuple(fields, is_dynamic)
optional EthereumABIValueInfo array = 4; // Array(element_type)
}
diff --git a/common/tests/fixtures/ethereum/sign_tx_clear_signing.json b/common/tests/fixtures/ethereum/sign_tx_clear_signing.json
index 69eea15a..d6793dda 100644
--- a/common/tests/fixtures/ethereum/sign_tx_clear_signing.json
+++ b/common/tests/fixtures/ethereum/sign_tx_clear_signing.json
@@ -172,7 +172,7 @@
"skip_models": ["t1b1"],
"parameters": {
"comment": "Trezor Test Paths descriptor (debug-only built-in): composite path styles - packed-bytes slicing and nested array-of-structs (including negative index + native currency). Bound to a dummy contract on mainnet via synthetic selector 7e577e04.",
- "data": "7e577e0400000000000000000000000000000000000000000000000000000000001e8480000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000028444444444444444444444444444444444444444455555555555555555555555555555555555555550000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000006666666666666666666666666666666666666666000000000000000000000000777777777777777777777777777777777777777700000000000000000000000000000000000000000000000000000000005b8d80000000000000000000000000888888888888888888888888888888888888888800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a7640000",
+ "data": "7e577e0400000000000000000000000000000000000000000000000000000000001e8480000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000284444444444444444444444444444444444444444555555555555555555555555555555555555555500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000006666666666666666666666666666666666666666000000000000000000000000777777777777777777777777777777777777777700000000000000000000000000000000000000000000000000000000005b8d80000000000000000000000000888888888888888888888888888888888888888800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a7640000",
"path": "m/44'/60'/0'/0/0",
"to_address": "0xdddddddddddddddddddddddddddddddddddddddd",
"chain_id": 1,
@@ -183,9 +183,9 @@
"value": "0x0"
},
"result": {
- "sig_v": 38,
- "sig_r": "f370341cda381d24db69507a38dafe542a816de081c47e7fd7655fe979644889",
- "sig_s": "1898b34ee2d012b19354c6c4b031747a4e6c371ed38a8e0d07d0e56414a6decd"
+ "sig_v": 37,
+ "sig_r": "c18b8cf0163fa2f6fdaec858735e9edfe0f17344c5637f7695c28c716df4d291",
+ "sig_s": "1b5faf83d9f6a999f0124035ddf3ca74bba35a71906ecb9925a4006763ae2710"
}
}
]
diff --git a/core/src/apps/ethereum/clear_signing.py b/core/src/apps/ethereum/clear_signing.py
index 85931371..3aa1a58b 100644
--- a/core/src/apps/ethereum/clear_signing.py
+++ b/core/src/apps/ethereum/clear_signing.py
@@ -523,9 +523,40 @@ class ABIValue:
Encoding reference: the Solidity ABI specification,
https://docs.soliditylang.org/en/latest/abi-spec.html#formal-specification-of-the-encoding
+
+ Per the spec, every type is either static or dynamic (`is_dynamic`):
+
+ * a static value is encoded in place, occupying `head_size` bytes;
+ * a dynamic value's head is a single word holding the offset of its
+ body, relative to the start of the enclosing block.
+
+ `parse` implements this head rule once for all types; subclasses only
+ describe what their body looks like by implementing `parse_body`.
"""
- def parse(self, raw_data: memoryview, offset: int) -> tuple[AnyValue, int]:
+ is_dynamic = False
+ head_size = _EVM_WORD_SIZE
+
+ def parse(
+ self, raw_data: memoryview, offset: int, block_start: int = 0
+ ) -> tuple[AnyValue, int]:
+ """Parse one value whose head is at `offset`.
+
+ `block_start` is where the enclosing block starts; offsets inside
+ heads are relative to it. Relevant for arrays, etc.
+ Returns the parsed value and the head bytes consumed.
+ """
+ if offset + self.head_size > len(raw_data):
+ raise OutOfBounds
+ if self.is_dynamic:
+ # the head of any dynamic type is one word: the offset of its body
+ # Dynamic types must have a parse_body method.
+ pointer = int.from_bytes(raw_data[offset : offset + _EVM_WORD_SIZE], "big")
+ return self.parse_body(raw_data, block_start + pointer), self.head_size
+ return self.parse_body(raw_data, offset), self.head_size
+
+ def parse_body(self, raw_data: memoryview, pos: int) -> AnyValue:
+ """Parse the value body located directly at `pos` (no indirection)."""
raise NotImplementedError
@staticmethod
@@ -546,10 +577,15 @@ class ABIValue:
elif element.dynamic is not None:
return Array(DynamicLeaf(_get_parser(element.dynamic, is_dynamic=True)))
elif element.tuple is not None:
+ fields = tuple(_get_leaf_parser(f) for f in element.tuple.fields)
+ # A non-array (leaf) struct/tuple is dynamic if any of its fields is dynamic.
+ # E.g. of dynamic members: bytes, string, uint256[], bytes[], bytes[][] etc.
+ # An array (this outer structure) is always* dynamic regardless of its fields.
+ # (*Unless it's of fixed length, which generally don't exist.)
return Array(
Tuple(
- tuple(_get_leaf_parser(f) for f in element.tuple.fields),
- is_dynamic=False, # Tuples inside Arrays are always parsed as static!
+ fields,
+ is_dynamic=any(p in DYNAMIC_DATA_PARSERS for p in fields),
)
)
elif element.array is not None:
@@ -568,18 +604,15 @@ class ABIValue:
class Atomic(ABIValue):
- """Atomic values, such as integers or addresses, are always stored on one EVM word."""
+ """Atomic values, such as integers or addresses, are static types
+ always stored on one EVM word."""
def __init__(self, parser: Parser) -> None:
self.parser = parser
- def parse(self, raw_data: memoryview, offset: int) -> tuple[AnyValue, int]:
- if offset + _EVM_WORD_SIZE > len(raw_data):
- raise OutOfBounds
- return (
- self.parser(raw_data[offset : offset + _EVM_WORD_SIZE]),
- _EVM_WORD_SIZE,
- )
+ def parse_body(self, raw_data: memoryview, pos: int) -> AnyValue:
+ # bounds already ensured by `parse`: pos + head_size <= len(raw_data)
+ return self.parser(raw_data[pos : pos + _EVM_WORD_SIZE])
def _read_dynamic_data(raw_data: memoryview, pointer: int) -> memoryview:
@@ -595,152 +628,96 @@ def _read_dynamic_data(raw_data: memoryview, pointer: int) -> memoryview:
class DynamicLeaf(ABIValue):
- """Dynamic leaf values, such as strings or `bytes`, are stored later in the
- calldata, the inline value being just a pointer to the actual location.
- Also they have an arbitrary length, which is encoded on the first word,
- after which the actual value follows."""
+ """`strings` or `bytes`. Their body is a
+ one-word byte length followed by that many bytes of data."""
+
+ is_dynamic = True
def __init__(self, parser: Parser) -> None:
self.parser = parser
- def parse(self, raw_data: memoryview, offset: int) -> tuple[AnyValue, int]:
- if offset + _EVM_WORD_SIZE > len(raw_data):
- raise OutOfBounds
- pointer = int.from_bytes(raw_data[offset : offset + _EVM_WORD_SIZE], "big")
- return self.parse_body(raw_data, pointer), _EVM_WORD_SIZE
-
- def parse_body(self, raw_data: memoryview, body_start: int) -> AnyValue:
- """Parse the length-prefixed value located directly at `body_start`.
-
- Unlike `parse`, this expects `body_start` to already point at the value
- body (no pointer indirection). Used when an enclosing `Array` has
- already resolved the element's absolute offset."""
- return self.parser(_read_dynamic_data(raw_data, body_start))
+ def parse_body(self, raw_data: memoryview, pos: int) -> AnyValue:
+ return self.parser(_read_dynamic_data(raw_data, pos))
class Tuple(ABIValue):
- """Tuples (or Structs, which are essentially the same thing as far as ABI is concerned)
- contain multiple values of different types.
- A Tuple is "dynamic" if at least one of the values is dynamic.
- However, dynamic structs inside arrays behave as static structs,
- hence we cannot guess if the Tuple is dynamic by looking at just its fields."""
+ """Tuples (or Structs - the same thing as far as the ABI is concerned)
+ contain multiple values of possibly different types. Only leaf fields
+ (atomic types, `bytes`, `string`) are supported here i.e. no more nesting; the dynamic
+ fields are the ones whose parser is in DYNAMIC_DATA_PARSERS.
+
+ A tuple is a dynamic type iff at least one of its fields is dynamic.
+ Its body is the concatenation of its fields' heads: a static field is
+ encoded in place, while a dynamic field's head is the offset of its
+ length-prefixed data, relative to the body start. A static tuple is
+ therefore just its field values back to back - no length prefix and no
+ offset words anywhere."""
def __init__(self, fields: tuple[Parser, ...], is_dynamic: bool) -> None:
+ if not fields:
+ # Zero-field structs do not exist in Solidity. Rejecting them here
+ # also guarantees head_size >= _EVM_WORD_SIZE for every type: a
+ # static tuple with head_size == 0 inside an Array would defeat
+ # the heads bounds pre-check (array_length * 0) and let an
+ # attacker-controlled length word drive an unbounded parse loop.
+ raise InvalidFormatDefinition
self.fields = fields
self.is_dynamic = is_dynamic
- self.static_size = len(fields) * _EVM_WORD_SIZE
-
- def parse(self, raw_data: memoryview, offset: int) -> tuple[TupleValue, int]:
- if not self.is_dynamic:
- base_offset = offset
- consumed = self.static_size
- else:
- if offset + _EVM_WORD_SIZE > len(raw_data):
- raise OutOfBounds
- pointer = int.from_bytes(raw_data[offset : offset + _EVM_WORD_SIZE], "big")
- base_offset = pointer
- consumed = _EVM_WORD_SIZE # dynamic structs just consume the pointer
+ self.fields_size = len(fields) * _EVM_WORD_SIZE
+ if not is_dynamic:
+ # a static tuple is encoded in place, so its head is its whole body
+ self.head_size = self.fields_size
- if base_offset + self.static_size > len(raw_data):
+ def parse_body(self, raw_data: memoryview, pos: int) -> AnyValue:
+ if pos + self.fields_size > len(raw_data):
raise OutOfBounds
value: list[Value] = [None] * len(self.fields)
for i, parser in enumerate(self.fields):
- field_head_pos = base_offset + (i * _EVM_WORD_SIZE)
+ field_head_pos = pos + (i * _EVM_WORD_SIZE)
raw_field = raw_data[field_head_pos : field_head_pos + _EVM_WORD_SIZE]
if parser not in DYNAMIC_DATA_PARSERS:
value[i] = parser(raw_field)
else:
- field_pointer = base_offset + int.from_bytes(raw_field, "big")
- raw_field = _read_dynamic_data(raw_data, field_pointer)
- value[i] = parser(raw_field)
- return tuple(value), consumed
+ field_pointer = pos + int.from_bytes(raw_field, "big")
+ value[i] = parser(_read_dynamic_data(raw_data, field_pointer))
+ return tuple(value)
class Array(ABIValue):
- """Arrays are sequences of value of the same type."""
+ """Arrays (`T[]`) are sequences of values of the same type, and are
+ always dynamic types themselves. The body is a one-word element count
+ followed by the elements' heads - in place values for static element
+ types (e.g. `uint256[]`, or an array of static structs, laid out at a
+ stride of the element's `head_size`), or one-word body offsets for
+ dynamic element types (e.g. `bytes[]`, `uint256[][]`, or an array of
+ structs containing a dynamic field)."""
+
+ is_dynamic = True
def __init__(self, element_definition: ABIValue) -> None:
self.element_definition = element_definition
- def parse(self, raw_data: memoryview, offset: int) -> tuple[ListValue, int]:
- if offset + _EVM_WORD_SIZE > len(raw_data):
+ def parse_body(self, raw_data: memoryview, pos: int) -> AnyValue:
+ if pos + _EVM_WORD_SIZE > len(raw_data):
raise OutOfBounds
- array_pointer = int.from_bytes(
- raw_data[offset : offset + _EVM_WORD_SIZE], "big"
- )
- return self._parse_body(raw_data, array_pointer), _EVM_WORD_SIZE
-
- def _parse_body(self, raw_data: memoryview, array_start: int) -> ListValue:
- if array_start + _EVM_WORD_SIZE > len(raw_data):
- raise OutOfBounds
- array_length = int.from_bytes(
- raw_data[array_start : array_start + _EVM_WORD_SIZE], "big"
- )
+ array_length = int.from_bytes(raw_data[pos : pos + _EVM_WORD_SIZE], "big")
+ element = self.element_definition
# element heads are laid out right after the length word, and any
# offsets among them are relative to this position
- elements_start = array_start + _EVM_WORD_SIZE
- array_heads_end = elements_start + (array_length * _EVM_WORD_SIZE)
- if array_heads_end > len(raw_data):
+ elements_start = pos + _EVM_WORD_SIZE
+ if elements_start + (array_length * element.head_size) > len(raw_data):
raise OutOfBounds
value = []
-
- for i in range(array_length):
- element_head_offset = elements_start + (i * _EVM_WORD_SIZE)
- if element_head_offset + _EVM_WORD_SIZE > len(raw_data):
- raise OutOfBounds
- if isinstance(self.element_definition, Atomic):
- # e.g. `uint256[]` / `address[]`: each element is a static leaf,
- # encoded in place (no pointer indirection), so `parse` reads the
- # one-word value directly at the element head position.
- data, _ = self.element_definition.parse(raw_data, element_head_offset)
- elif isinstance(self.element_definition, Array):
- # e.g. `uint256[][]`: each element is itself a (dynamic) array, so
- # the element head is a relative offset to the inner array body.
- # Dereference it, then `_parse_body` consumes the inner length
- # prefix -- this is the array form of the "dynamic dance".
- element_pointer = int.from_bytes(
- raw_data[
- element_head_offset : element_head_offset + _EVM_WORD_SIZE
- ],
- "big",
- )
- inner_array_start = elements_start + element_pointer
- data = self.element_definition._parse_body(raw_data, inner_array_start)
- elif isinstance(self.element_definition, DynamicLeaf):
- # e.g. `bytes[]` / `string[]`: each element is a dynamic leaf, so
- # the element head is a relative offset to the length-prefixed
- # element body. Read the body directly, mirroring the inner-array
- # case above -- calling `parse` here would dereference the offset
- # a second time.
- element_pointer = int.from_bytes(
- raw_data[
- element_head_offset : element_head_offset + _EVM_WORD_SIZE
- ],
- "big",
- )
- element_absolute_pointer = elements_start + element_pointer
- data = self.element_definition.parse_body(
- raw_data, element_absolute_pointer
- )
- else:
- # e.g. `MyStruct[]`: each element is a struct (Tuple). Inside an
- # array a struct is encoded via a relative offset head (and parsed
- # as static -- see `from_proto`), so dereference then `parse`.
- element_pointer = int.from_bytes(
- raw_data[
- element_head_offset : element_head_offset + _EVM_WORD_SIZE
- ],
- "big",
- )
- element_absolute_pointer = elements_start + element_pointer
- data, _ = self.element_definition.parse(
- raw_data, element_absolute_pointer
- )
+ element_head_offset = elements_start
+ for _ in range(array_length):
+ data, consumed = element.parse(
+ raw_data, element_head_offset, elements_start
+ )
value.append(data)
-
+ element_head_offset += consumed
return value
diff --git a/core/tests/test_apps.ethereum.clear_signing.py b/core/tests/test_apps.ethereum.clear_signing.py
index 9dd4c665..83daf496 100644
--- a/core/tests/test_apps.ethereum.clear_signing.py
+++ b/core/tests/test_apps.ethereum.clear_signing.py
@@ -282,8 +282,9 @@ class TestEthereumClearSigning(unittest.TestCase):
array_parser = Array(
Tuple(
(parse_address, parse_string),
- # Note: dynamic structs that sit inside arrays behave as static structs
- is_dynamic=False,
+ # the `string` field makes the struct a dynamic type, so the
+ # array encodes its elements via offset heads
+ is_dynamic=True,
)
)
@@ -327,6 +328,66 @@ class TestEthereumClearSigning(unittest.TestCase):
self.assertEqual(parsed, [(addr1, text1), (addr2, text2)])
self.assertEqual(consumed, 32)
+ def test_array_of_static_structs(self):
+ # (address,address,uint256)[]: the struct has no dynamic fields, so it
+ # is a static type - array elements are encoded IN PLACE right after
+ # the length word, at a stride of the struct size (96 bytes), with no
+ # offset heads at all.
+ array_parser = Array(
+ Tuple(
+ (parse_address, parse_address, parse_uint256),
+ is_dynamic=False,
+ )
+ )
+
+ addr1 = unhexlify("6666666666666666666666666666666666666666")
+ addr2 = unhexlify("7777777777777777777777777777777777777777")
+ addr3 = unhexlify("8888888888888888888888888888888888888888")
+ addr4 = unhexlify("9999999999999999999999999999999999999999")
+
+ def pad_addr(addr):
+ return b"\x00" * (32 - 20) + addr
+
+ array_pointer = len(FIVE_RANDOM_BYTES) + 32 # absolute pos of array body
+ payload = (
+ to_bytes(array_pointer)
+ + to_bytes(2) # element count; elements follow in place
+ + pad_addr(addr1) # element 0
+ + pad_addr(addr2)
+ + to_bytes(6000000)
+ + pad_addr(addr3) # element 1
+ + pad_addr(addr4)
+ + to_bytes(1500000)
+ )
+ data = memoryview(FIVE_RANDOM_BYTES + payload + SEVEN_RANDOM_BYTES)
+
+ parsed, consumed = array_parser.parse(data, len(FIVE_RANDOM_BYTES))
+
+ self.assertEqual(parsed, [(addr1, addr2, 6000000), (addr3, addr4, 1500000)])
+ self.assertEqual(consumed, 32) # only the array's own offset head
+
+ # count claims two elements but only one is present: the heads area
+ # (2 * 96 bytes) fails the bounds pre-check
+ truncated = (
+ to_bytes(32) # pointer to array body (absolute pos, no prefix)
+ + to_bytes(2)
+ + pad_addr(addr1)
+ + pad_addr(addr2)
+ + to_bytes(6000000)
+ )
+ with self.assertRaises(OutOfBounds):
+ array_parser.parse(memoryview(truncated), 0)
+
+ def test_empty_tuple_rejected(self):
+ # Zero-field structs don't exist in Solidity. A static one would have
+ # head_size == 0, and inside an Array that zero stride would defeat
+ # the heads bounds pre-check (array_length * 0) and let an
+ # attacker-controlled length word drive an unbounded parse loop.
+ with self.assertRaises(InvalidFormatDefinition):
+ Tuple((), is_dynamic=False)
+ with self.assertRaises(InvalidFormatDefinition):
+ Tuple((), is_dynamic=True)
+
def test_bytes32_parsing(self):
atomic_bytes32 = Atomic(parse_bytes32)
diff --git a/tests/device_tests/ethereum/test_definitions_request.py b/tests/device_tests/ethereum/test_definitions_request.py
index 4d6b54b4..d3e0208b 100644
--- a/tests/device_tests/ethereum/test_definitions_request.py
+++ b/tests/device_tests/ethereum/test_definitions_request.py
@@ -322,23 +322,24 @@ TEST_TOKEN_CALLDATA = bytes.fromhex(
)
# Paths descriptor: packedPath 0x44..||0x55.., swapData
# [(0x66,0x77,6.0),(0x88,native,1.5 ETH)] - native sentinel is never requested.
+# swapData is `(address,address,uint256)[]`. The struct has no dynamic fields,
+# so it is a STATIC type: per the ABI spec its elements are encoded in place
+# right after the element count (stride 96), with no offset heads.
TEST_PATHS_CALLDATA = bytes.fromhex(
"7e577e04"
- "00000000000000000000000000000000000000000000000000000000001e8480"
- "0000000000000000000000000000000000000000000000000000000000000060"
- "00000000000000000000000000000000000000000000000000000000000000c0"
- "0000000000000000000000000000000000000000000000000000000000000028"
- "4444444444444444444444444444444444444444555555555555555555555555"
- "5555555555555555000000000000000000000000000000000000000000000000"
- "0000000000000000000000000000000000000000000000000000000000000002"
- "0000000000000000000000000000000000000000000000000000000000000040"
- "00000000000000000000000000000000000000000000000000000000000000a0"
- "0000000000000000000000006666666666666666666666666666666666666666"
- "0000000000000000000000007777777777777777777777777777777777777777"
- "00000000000000000000000000000000000000000000000000000000005b8d80"
- "0000000000000000000000008888888888888888888888888888888888888888"
- "0000000000000000000000000000000000000000000000000000000000000000"
- "00000000000000000000000000000000000000000000000014d1120d7b160000"
+ "00000000000000000000000000000000000000000000000000000000001e8480" # amount = 2000000
+ "0000000000000000000000000000000000000000000000000000000000000060" # offset of packedPath body
+ "00000000000000000000000000000000000000000000000000000000000000c0" # offset of swapData body
+ "0000000000000000000000000000000000000000000000000000000000000028" # packedPath byte length = 40
+ "4444444444444444444444444444444444444444555555555555555555555555" # packedPath data...
+ "5555555555555555000000000000000000000000000000000000000000000000" # ...and zero padding
+ "0000000000000000000000000000000000000000000000000000000000000002" # swapData element count = 2
+ "0000000000000000000000006666666666666666666666666666666666666666" # [0].sendingAssetId
+ "0000000000000000000000007777777777777777777777777777777777777777" # [0].receivingAssetId
+ "00000000000000000000000000000000000000000000000000000000005b8d80" # [0].fromAmount = 6000000
+ "0000000000000000000000008888888888888888888888888888888888888888" # [1].sendingAssetId
+ "0000000000000000000000000000000000000000000000000000000000000000" # [1].receivingAssetId (native)
+ "00000000000000000000000000000000000000000000000014d1120d7b160000" # [1].fromAmount = 1.5 ETH
)
# Both token-bearing descriptors, signed in turn by each token-request test.
TEST_DESCRIPTOR_CALLDATAS = (TEST_TOKEN_CALLDATA, TEST_PATHS_CALLDATA)
Why this scored 62/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.