fix(ethereum): render unoswap pools as raw bytes
What changed, and why it matters
This commit fixes how Trezor displays details for certain 1inch cryptocurrency swap transactions. Previously, the device tried to interpret a parameter called 'pools' as a list of plain numbers, but it actually contains packed binary data. The change makes the device show the raw bytes as hexadecimal instead of trying to decode them as numbers. This is a display/clarity fix rather than a direct theft-of-funds bug, but mis-displaying swap details could in theory help an attacker trick a user into approving a malicious transaction.
Users relying on Trezor's clear-signing for 1inch swaps should ensure their firmware is updated so that pool data is shown accurately. Developers should verify whether other functions in the clear-signing registry have similar type mismatches and consider adding regression tests for calldata decoding. No immediate emergency action is indicated.
Security signals we found
Transaction display misclassification: a bytes field was being parsed as uint256 array, potentially showing misleading information to the user
Clear-signing registry mismatch: the EIP-7730 registry description did not match actual 1inch calldata encoding
User confirmation UI change: previously only the last pool was shown with a unit formatter; now the full raw pools bytes are shown as hex
No changelog entry indicates the developer did not treat this as a user-visible security fix
Evidence from the diff
The patch updates Ethereum clear-signing definitions for 1inch’s unoswap/unoswapTo functions. The ‘pools’ argument was declared as uint256[] and rendered via parse_uint256_array / UnitFormatter as the last pool. In reality the on-chain parameter is a single bytes field that encodes multiple pools in a non-standard, gas-optimized format. The commit adds a RawBytesFormatter and switches the parameter to Atomic(parse_bytes), rendering the entire pools value as hex. It removes the previous array parsing path. This is a correctness improvement for transaction decoding and display.
Changed components
core/src/apps/ethereum/clear_signing.pycore/src/apps/ethereum/clear_signing_definitions.py1inch unoswap/unoswapTo clear-signing display on Trezor devicesInspect captured patch +39 / −11
diff --git a/core/src/apps/ethereum/clear_signing.py b/core/src/apps/ethereum/clear_signing.py
index 9c188f09..5f2b6c3a 100644
--- a/core/src/apps/ethereum/clear_signing.py
+++ b/core/src/apps/ethereum/clear_signing.py
@@ -1,5 +1,6 @@
from micropython import const
from typing import TYPE_CHECKING
+from ubinascii import hexlify
from trezor import TR
from trezor.utils import BufferReader
@@ -275,6 +276,27 @@ class UnitFormatter(FieldFormatter):
return f"{significand:g}{prefix_symbol}{self.base}", None, None
+class RawBytesFormatter(FieldFormatter):
+ """HACK: this is currently used to just dump some parameters
+ that encode more information in non-standard ways (see unoswap and unoswapTo from 1inch).
+ """
+
+ def format(
+ self,
+ raw_value: AnyValue,
+ _definitions: Definitions,
+ _token: EthereumTokenInfo,
+ _path_Walker: PathWalker,
+ ) -> tuple[str | None, EthereumTokenInfo | None, bytes | None]:
+ if raw_value is None:
+ return None, None, None
+ else:
+ if not isinstance(raw_value, bytes):
+ raise InvalidFormatDefinition
+
+ return hexlify(raw_value).decode(), None, None
+
+
# https://eips.ethereum.org/EIPS/eip-7730#context-section
diff --git a/core/src/apps/ethereum/clear_signing_definitions.py b/core/src/apps/ethereum/clear_signing_definitions.py
index b1379532..3c8fec38 100644
--- a/core/src/apps/ethereum/clear_signing_definitions.py
+++ b/core/src/apps/ethereum/clear_signing_definitions.py
@@ -13,6 +13,7 @@ from .clear_signing import (
DisplayFormat,
Dynamic,
FieldDefinition,
+ RawBytesFormatter,
Struct,
TokenAmountFormatter,
UnitFormatter,
@@ -23,7 +24,6 @@ from .clear_signing import (
parse_uint24,
parse_uint160,
parse_uint256,
- parse_uint256_array,
)
# https://github.com/LedgerHQ/clear-signing-erc7730-registry/blob/master/ercs/calldata-erc20-tokens.json#L27
@@ -140,13 +140,13 @@ ALL_DISPLAY_FORMATS.extend(
binding_context=ONEINCH_CONTEXT,
func_sig=unhexlify(
"83800a8e"
- ), # unoswap(address srcToken, uint256 amount, uint256 minReturn, uint256[] pools)
+ ), # unoswap(address srcToken, uint256 amount, uint256 minReturn, bytes pools)
intent="Swap",
parameter_definitions=[
Atomic(parse_address), # srcToken
Atomic(parse_uint256), # amount
Atomic(parse_uint256), # minReturn
- Dynamic(parse_uint256_array), # pools
+ Atomic(parse_bytes), # pools
],
field_definitions=[
FieldDefinition(
@@ -167,9 +167,12 @@ ALL_DISPLAY_FORMATS.extend(
AddressNameFormatter,
),
FieldDefinition(
- (3, -1), # pools.[-1]
- "Last pool",
- UnitFormatter,
+ (3,), # pools
+ "Pools (hex)",
+ # HACK: this parameter encodes data in a non-standard way
+ # in order to extract human readable data from it,
+ # we would need to parse this further
+ RawBytesFormatter,
),
],
),
@@ -177,14 +180,14 @@ ALL_DISPLAY_FORMATS.extend(
binding_context=ONEINCH_CONTEXT,
func_sig=unhexlify(
"e2c95c82"
- ), # unoswapTo(address recipient, address srcToken, uint256 amount, uint256 minReturn, uint256[] pools)
+ ), # unoswapTo(address recipient, address srcToken, uint256 amount, uint256 minReturn, bytes pools)
intent="Swap",
parameter_definitions=[
Atomic(parse_address), # recipient
Atomic(parse_address), # srcToken
Atomic(parse_uint256), # amount
Atomic(parse_uint256), # minReturn
- Dynamic(parse_uint256_array), # pools
+ Atomic(parse_bytes), # pools
],
field_definitions=[
FieldDefinition(
@@ -205,9 +208,12 @@ ALL_DISPLAY_FORMATS.extend(
AddressNameFormatter,
),
FieldDefinition(
- (4, -1), # pools.[-1]
- "Last pool",
- UnitFormatter,
+ (4,), # pools
+ "Pools (hex)",
+ # HACK: this parameter encodes data in a non-standard way
+ # in order to extract human readable data from it,
+ # we would need to parse this further
+ RawBytesFormatter,
),
],
),
Why this scored 37/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.