feat(clear_signing): Add enum formatter - core
What changed, and why it matters
This commit adds a new display formatter for Ethereum clear signing. It lets a transaction descriptor map numeric enum values (like 1 or 2) to human-readable labels (like 'stable' or 'variable') on the Trezor screen. The change is purely additive and includes tests. There is no indication it fixes a security bug or introduces a vulnerability; it is a feature implementation.
No security action required. Review the feature for product correctness and ensure the new EthereumERC7730FieldInfo and EthereumERC7730EnumEntry protobuf messages are present and validated upstream.
Security signals we found
New formatter raises InvalidFormatDefinition on unexpected/missing enum values, causing fallback to blind signing rather than displaying an untrusted label
Duplicate enum keys are rejected at descriptor decode time
Non-integer enum values are rejected at format time
No memory-unsafe operations or external I/O added
Evidence from the diff
The patch introduces EnumFormatter in core/src/apps/ethereum/clear_signing.py to implement the ERC-7730 ‘enum’ formatter type. It validates that the supplied value is an integer, looks it up in a descriptor-provided dict[int, str], and returns the mapped string. Missing keys and non-integer values raise InvalidFormatDefinition, which causes the device to fall back to blind signing. FieldDefinition.from_proto() is updated to build the formatter from EthereumERC7730FieldInfo.enum_values, rejecting duplicate keys and missing mappings. Unit tests cover normal mapping, None passthrough, missing keys, non-integer inputs, array rendering, and end-to-end calldata parsing.
Changed components
core/src/apps/ethereum/clear_signing.pycore/tests/test_apps.ethereum.clear_signing.pyEthereum clear-signing / ERC-7730 display pathInspect captured patch +131 / −4
diff --git a/core/src/apps/ethereum/clear_signing.py b/core/src/apps/ethereum/clear_signing.py
index a2b06bdf..8f0507a5 100644
--- a/core/src/apps/ethereum/clear_signing.py
+++ b/core/src/apps/ethereum/clear_signing.py
@@ -96,9 +96,6 @@ class InvalidFormatDefinition(ClearSigningFailed):
pass
-# Value Parsers
-
-
def _check_padding_zero(
raw_data: memoryview, used_bytes: int, exc: type[ValueOverflow] = ValueOverflow
) -> None:
@@ -517,6 +514,28 @@ class CalldataFormatter(RawFormatter):
def __init__(self, callee_path: Path, selector: bytes | None = None) -> None:
self.callee_path = callee_path
self.selector = selector
+class EnumFormatter(FieldFormatter):
+ """ERC-7730 `enum` format: the value read from calldata is a key into a
+ descriptor-supplied mapping and is displayed as the mapped string."""
+
+ def __init__(self, entries: dict[int, str]) -> None:
+ self.entries = entries
+
+ async def format(
+ self,
+ value: AnyValue,
+ _msg: MsgInSignTx,
+ _definitions: Definitions,
+ _path_walker: PathWalker,
+ ) -> tuple[str | AboveThreshold | None, EthereumTokenInfo | None, AnyBytes | None]:
+ if value is None:
+ return None, None, None
+ if not isinstance(value, int):
+ raise InvalidFormatDefinition
+ formatted = self.entries.get(value)
+ if formatted is None:
+ raise InvalidFormatDefinition
+ return formatted, None, None
async def _format_field_value(
@@ -863,6 +882,15 @@ class FieldDefinition:
if selector is not None and len(selector) != SC_FUNC_SIG_BYTES:
raise InvalidFormatDefinition
formatter = CalldataFormatter(decode_path(info.callee_path), selector)
+ elif fmt_type == FT.FORMATTER_ENUM:
+ if not info.enum_values:
+ raise InvalidFormatDefinition
+ enum_entries: dict[int, str] = {}
+ for e in info.enum_values:
+ if e.key in enum_entries:
+ raise InvalidFormatDefinition
+ enum_entries[e.key] = e.value
+ formatter = EnumFormatter(enum_entries)
else:
raise InvalidFormatDefinition
diff --git a/core/tests/test_apps.ethereum.clear_signing.py b/core/tests/test_apps.ethereum.clear_signing.py
index ac659bf5..8368ac6b 100644
--- a/core/tests/test_apps.ethereum.clear_signing.py
+++ b/core/tests/test_apps.ethereum.clear_signing.py
@@ -9,7 +9,11 @@ if not utils.BITCOIN_ONLY:
from ethereum_common import *
from trezor.enums import EthereumERC7730FieldFormatterType as FT
- from trezor.messages import EthereumERC7730FieldInfo, EthereumERC7730Path
+ from trezor.messages import (
+ EthereumERC7730EnumEntry,
+ EthereumERC7730FieldInfo,
+ EthereumERC7730Path,
+ )
from apps.ethereum import clear_signing_definitions
from apps.ethereum.clear_signing import (
@@ -22,6 +26,7 @@ if not utils.BITCOIN_ONLY:
DirtyAddress,
DisplayFormat,
DynamicLeaf,
+ EnumFormatter,
FieldDefinition,
InvalidFormatDefinition,
OutOfBounds,
@@ -631,6 +636,100 @@ class TestEthereumClearSigning(unittest.TestCase):
formatted, _, _ = await_result(date_fmt.format(1616051824, None, None, None))
self.assertEqual(formatted, "2021-03-18 07:17:04")
+ # --- enum formatter ---
+
+ def test_enum_formatter(self):
+ # ERC-7730 `enum` format: an ABI-registered enum for interestRateMode
+ # in Aave's supply/borrow calls.
+ fmt = EnumFormatter({1: "stable", 2: "variable"})
+
+ formatted, token, addr = await_result(fmt.format(1, None, None, None))
+ self.assertEqual(formatted, "stable")
+ self.assertIsNone(token)
+ self.assertIsNone(addr)
+
+ formatted, _, _ = await_result(fmt.format(2, None, None, None))
+ self.assertEqual(formatted, "variable")
+
+ # None -> None
+ formatted, _, _ = await_result(fmt.format(None, None, None, None))
+ self.assertIsNone(formatted)
+
+ # a key missing from the mapping fails clear signing
+ with self.assertRaises(InvalidFormatDefinition):
+ await_result(fmt.format(3, None, None, None))
+
+ # non-integer values are rejected
+ with self.assertRaises(InvalidFormatDefinition):
+ await_result(fmt.format("stable", None, None, None))
+ with self.assertRaises(InvalidFormatDefinition):
+ await_result(fmt.format(b"\x01", None, None, None))
+
+ # an enum pointed at an array renders one entry per line
+ formatted, _, _ = await_result(
+ _format_field_value(fmt, [2, 1], None, None, None)
+ )
+ self.assertEqual(formatted, "variable\nstable")
+
+ def test_from_proto_enum_dispatch(self):
+ info = EthereumERC7730FieldInfo(
+ path=EthereumERC7730Path(path=[0]),
+ label="Interest rate mode",
+ formatter=FT.FORMATTER_ENUM,
+ enum_values=[
+ EthereumERC7730EnumEntry(key=1, value="stable"),
+ EthereumERC7730EnumEntry(key=2, value="variable"),
+ ],
+ )
+ fmt = FieldDefinition.from_proto(info).get_formatter()
+ self.assertIsInstance(fmt, EnumFormatter)
+ formatted, _, _ = await_result(fmt.format(2, None, None, None))
+ self.assertEqual(formatted, "variable")
+
+ # an enum field without its mapping is rejected at decode time
+ info = EthereumERC7730FieldInfo(
+ path=EthereumERC7730Path(path=[0]),
+ label="Interest rate mode",
+ formatter=FT.FORMATTER_ENUM,
+ )
+ with self.assertRaises(InvalidFormatDefinition):
+ FieldDefinition.from_proto(info)
+
+ def test_enum_end_to_end(self):
+ # Full path: calldata -> parse a `uint8` parameter -> the field renders
+ # the mapped display string.
+ def make_display_format():
+ return DisplayFormat(
+ binding_context=None,
+ func_sig=b"\x00\x00\x00\x00",
+ provider_name=None,
+ intent="Test",
+ parameter_definitions=[Atomic(make_uint_parser(8))],
+ field_definitions=[
+ FieldDefinition(
+ (0,), "Rate mode", EnumFormatter({1: "stable", 2: "variable"})
+ )
+ ],
+ )
+
+ parameters, fields = await_result(
+ make_display_format().parse_calldata(memoryview(to_bytes(2)), None, None)
+ )
+ self.assertEqual(parameters, [2])
+ (label, formatted, _), token, token_address = fields[0]
+ self.assertEqual(label, "Rate mode")
+ self.assertEqual(formatted, "variable")
+ self.assertIsNone(token)
+ self.assertIsNone(token_address)
+
+ # a calldata value outside the mapping fails the parse (blind signing)
+ with self.assertRaises(InvalidFormatDefinition):
+ await_result(
+ make_display_format().parse_calldata(
+ memoryview(to_bytes(3)), None, None
+ )
+ )
+
# --- Multi-value fields (a formatter pointed at an array) ---
def test_multi_value_formats_each_element(self):
Why this scored 21/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.