feat(ethereum): support ContainerPath.To
What changed, and why it matters
This commit adds support for a new 'To' container path in Trezor's Ethereum clear-signing feature. It lets the device reference the transaction recipient when formatting token amounts, which is useful for displaying ERC-20 transfers and approvals correctly. The change is a feature addition, not a clear security fix, and there is no evidence of a disclosed vulnerability.
No immediate security action required. Review the refactored formatter and path resolution logic during normal QA to ensure the new TO path does not introduce display spoofing or incorrect token-decimal handling.
Security signals we found
Feature addition to clear-signing path resolution
Refactoring of formatter APIs to pass full transaction context
No mention of vulnerability, bug, or security issue in commit message or diff
Evidence from the diff
The patch extends the EthereumERC7730ContainerPath enum with a TO value across protobuf, Python, Rust, and core enum definitions. It refactors DisplayFormat.parse into an async parse_calldata that takes the full MsgInSignTx object, allowing container path resolution to access msg.to. TokenAmountFormatter now requires a token_path and uses ContainerPath.To for APPROVE and TRANSFER display formats. The change is architectural/feature work; no vulnerability or exploit is described in the commit materials.
Changed components
core/src/apps/ethereum/clear_signing.pycore/src/apps/ethereum/clear_signing_definitions.pycommon/protob/messages-definitions.protocore/src/trezor/enums/EthereumERC7730ContainerPath.pycore/src/trezor/enums/__init__.pypython/src/trezorlib/messages.pyrust/trezor-client/src/protos/generated/messages_definitions.rsInspect captured patch +52 / −61
diff --git a/common/protob/messages-definitions.proto b/common/protob/messages-definitions.proto
index c3418656..f310c0c7 100644
--- a/common/protob/messages-definitions.proto
+++ b/common/protob/messages-definitions.proto
@@ -136,6 +136,7 @@ enum EthereumERC7730FieldFormatterType {
enum EthereumERC7730ContainerPath {
FROM = 1;
VALUE = 2;
+ TO = 3;
}
/**
diff --git a/core/src/apps/ethereum/clear_signing.py b/core/src/apps/ethereum/clear_signing.py
index b62dc5cc..9c39bcfe 100644
--- a/core/src/apps/ethereum/clear_signing.py
+++ b/core/src/apps/ethereum/clear_signing.py
@@ -271,7 +271,7 @@ class AmountFormatter(FieldFormatter):
class TokenAmountFormatter(FieldFormatter):
def __init__(
self,
- token_path: Path | None = None,
+ token_path: Path,
native_currency_address: list[bytes] | None = None,
threshold: int | None = None,
) -> None:
@@ -549,6 +549,7 @@ class Array(ABIValue):
class ContainerPath:
From = 1
Value = 2
+ To = 3
class FieldDefinition:
@@ -633,13 +634,11 @@ class DisplayFormat:
return self.binding_context.matches(chain_id, address)
- def parse(
+ async def parse_calldata(
self,
calldata: memoryview,
- address_n: list[int],
- tx_value: AnyBytes,
- definitions: Definitions,
- token: EthereumTokenInfo,
+ msg: MsgInSignTx,
+ defs: Definitions,
) -> tuple[
list[AnyValue],
list[tuple[StrPropertyType, EthereumTokenInfo | None, AnyBytes | None]],
@@ -656,10 +655,12 @@ class DisplayFormat:
if isinstance(path, int): # ContainerPath
# standard container paths like @.from, @.value...
if path == ContainerPath.From:
- account, _ = get_account_and_path(address_n)
+ account, _ = get_account_and_path(msg.address_n)
return account
elif path == ContainerPath.Value:
- return int.from_bytes(tx_value, "big")
+ return int.from_bytes(msg.value, "big")
+ elif path == ContainerPath.To:
+ return bytes_from_address(msg.to)
else:
raise NotImplementedError # TODO
else:
@@ -701,25 +702,17 @@ class DisplayFormat:
tuple[StrPropertyType, EthereumTokenInfo | None, AnyBytes | None]
] = []
for field_definition in self.field_definitions:
- (
- formatted_value,
- actual_token,
- actual_token_address,
- ) = field_definition.get_formatter().format(
- get_value_for_path(field_definition.path),
- definitions,
- token,
- get_value_for_path,
+ value = get_value_for_path(field_definition.path)
+ formatter = field_definition.get_formatter()
+
+ formatted, token, token_address = await formatter.format(
+ value, msg, defs, get_value_for_path
)
fields.append(
(
- (
- field_definition.label,
- formatted_value,
- None,
- ),
- actual_token,
- actual_token_address,
+ (field_definition.label, formatted, None),
+ token,
+ token_address,
)
)
@@ -788,7 +781,6 @@ async def try_parse(
return False
calldata = memoryview(data)[SC_FUNC_SIG_BYTES:]
- token = definitions.get_token(address_bytes)
# custom treatment of certain functions (APPROVE, TRANSFER)
if display_format.func_sig == APPROVE_DISPLAY_FORMAT.func_sig:
@@ -797,8 +789,7 @@ async def try_parse(
display_format,
address_bytes,
msg,
- definitions,
- token,
+ defs,
maximum_fee,
fee_items,
)
@@ -808,8 +799,7 @@ async def try_parse(
display_format,
address_bytes,
msg,
- definitions,
- token,
+ defs,
maximum_fee,
fee_items,
payment_request_verifier,
@@ -820,8 +810,7 @@ async def try_parse(
calldata,
display_format,
msg,
- definitions,
- token,
+ defs,
maximum_fee,
)
return True
@@ -832,8 +821,7 @@ async def _handle_approve(
display_format: DisplayFormat,
address_bytes: bytes,
msg: MsgInSignTx,
- definitions: Definitions,
- token: EthereumTokenInfo,
+ defs: Definitions,
maximum_fee: str,
fee_items: Iterable[StrPropertyType],
) -> None:
@@ -842,9 +830,7 @@ async def _handle_approve(
from .sc_constants import KNOWN_ADDRESSES
from .yielding_vaults import UNKNOWN_VAULT, lookup_vault
- args, fields = display_format.parse(
- calldata, msg.address_n, msg.value, definitions, token
- )
+ args, fields = await display_format.parse_calldata(calldata, msg, defs)
assert len(args) == 2
assert len(fields) == 2
@@ -862,7 +848,7 @@ async def _handle_approve(
recipient_str = KNOWN_ADDRESSES.get(arg0_raw_value)
if recipient_str is None:
- vault = lookup_vault(definitions.network, arg0_raw_value)
+ vault = lookup_vault(defs.network, arg0_raw_value)
if vault is not UNKNOWN_VAULT:
recipient_str = vault.name
@@ -876,8 +862,8 @@ async def _handle_approve(
maximum_fee,
fee_items,
msg.chain_id,
- definitions.network,
- token,
+ defs.network,
+ defs.get_token(address_bytes),
address_bytes,
is_revoke,
bool(msg.chunkify),
@@ -889,17 +875,14 @@ async def _handle_transfer(
display_format: DisplayFormat,
address_bytes: bytes,
msg: MsgInSignTx,
- definitions: Definitions,
- token: EthereumTokenInfo,
+ defs: Definitions,
maximum_fee: str,
fee_items: Iterable[StrPropertyType],
payment_request_verifier: PaymentRequestVerifier | None,
) -> None:
from .layout import require_confirm_payment_request, require_confirm_tx
- args, fields = display_format.parse(
- calldata, msg.address_n, msg.value, definitions, token
- )
+ args, fields = await display_format.parse_calldata(calldata, msg, defs)
assert len(args) == 2
assert len(fields) == 2
@@ -928,9 +911,9 @@ async def _handle_transfer(
maximum_fee,
fee_items,
msg.chain_id,
- definitions.network,
- token,
- address_from_bytes(address_bytes, definitions.network),
+ defs.network,
+ defs.get_token(address_bytes),
+ address_from_bytes(address_bytes, defs.network),
)
else:
await require_confirm_tx(
@@ -940,7 +923,7 @@ async def _handle_transfer(
msg.address_n,
maximum_fee,
fee_items,
- token,
+ defs.get_token(address_bytes),
is_send=True,
chunkify=bool(msg.chunkify),
)
@@ -950,8 +933,7 @@ async def _handle_generic_ui(
calldata: memoryview,
display_format: DisplayFormat,
msg: MsgInSignTx,
- definitions: Definitions,
- token: EthereumTokenInfo,
+ defs: Definitions,
maximum_fee: str,
) -> None:
from . import tokens
@@ -959,9 +941,7 @@ async def _handle_generic_ui(
from .layout import require_confirm_clear_signing
from .sc_constants import KNOWN_ADDRESSES
- _, fields = display_format.parse(
- calldata, msg.address_n, msg.value, definitions, token
- )
+ _, fields = await display_format.parse_calldata(calldata, msg, defs)
properties_to_confirm = []
@@ -969,9 +949,7 @@ async def _handle_generic_ui(
properties_to_confirm.append(field)
if actual_token is tokens.UNKNOWN_TOKEN:
assert actual_token_address is not None
- token_address_str = address_from_bytes(
- actual_token_address, definitions.network
- )
+ token_address_str = address_from_bytes(actual_token_address, defs.network)
token_address_property: StrPropertyType = (
TR.ethereum__token_contract,
token_address_str,
diff --git a/core/src/apps/ethereum/clear_signing_definitions.py b/core/src/apps/ethereum/clear_signing_definitions.py
index 7a8a95af..f16e44ca 100644
--- a/core/src/apps/ethereum/clear_signing_definitions.py
+++ b/core/src/apps/ethereum/clear_signing_definitions.py
@@ -41,7 +41,8 @@ APPROVE_DISPLAY_FORMAT = DisplayFormat(
(1,),
"Amount",
TokenAmountFormatter(
- threshold=0x8000000000000000000000000000000000000000000000000000000000000000
+ token_path=ContainerPath.To,
+ threshold=0x8000000000000000000000000000000000000000000000000000000000000000,
),
),
],
@@ -58,7 +59,9 @@ TRANSFER_DISPLAY_FORMAT = DisplayFormat(
],
field_definitions=[
FieldDefinition((0,), "To", AddressNameFormatter),
- FieldDefinition((1,), "Amount", TokenAmountFormatter),
+ FieldDefinition(
+ (1,), "Amount", TokenAmountFormatter(token_path=ContainerPath.To)
+ ),
],
)
diff --git a/core/src/trezor/enums/EthereumERC7730ContainerPath.py b/core/src/trezor/enums/EthereumERC7730ContainerPath.py
index 2ddcb86c..eecc6519 100644
--- a/core/src/trezor/enums/EthereumERC7730ContainerPath.py
+++ b/core/src/trezor/enums/EthereumERC7730ContainerPath.py
@@ -4,3 +4,4 @@
FROM = 1
VALUE = 2
+TO = 3
diff --git a/core/src/trezor/enums/__init__.py b/core/src/trezor/enums/__init__.py
index 32300bc3..b9c45e4f 100644
--- a/core/src/trezor/enums/__init__.py
+++ b/core/src/trezor/enums/__init__.py
@@ -321,6 +321,7 @@ if TYPE_CHECKING:
class EthereumERC7730ContainerPath(IntEnum):
FROM = 1
VALUE = 2
+ TO = 3
class EthereumDataType(IntEnum):
UINT = 1
diff --git a/python/src/trezorlib/messages.py b/python/src/trezorlib/messages.py
index c400a80b..9034bfdb 100644
--- a/python/src/trezorlib/messages.py
+++ b/python/src/trezorlib/messages.py
@@ -364,6 +364,7 @@ class EthereumERC7730FieldFormatterType(IntEnum):
class EthereumERC7730ContainerPath(IntEnum):
FROM = 1
VALUE = 2
+ TO = 3
class EthereumDataType(IntEnum):
diff --git a/rust/trezor-client/src/protos/generated/messages_definitions.rs b/rust/trezor-client/src/protos/generated/messages_definitions.rs
index ab3a2dec..5478f856 100644
--- a/rust/trezor-client/src/protos/generated/messages_definitions.rs
+++ b/rust/trezor-client/src/protos/generated/messages_definitions.rs
@@ -2635,6 +2635,8 @@ pub enum EthereumERC7730ContainerPath {
FROM = 1,
// @@protoc_insertion_point(enum_value:hw.trezor.messages.definitions.EthereumERC7730ContainerPath.VALUE)
VALUE = 2,
+ // @@protoc_insertion_point(enum_value:hw.trezor.messages.definitions.EthereumERC7730ContainerPath.TO)
+ TO = 3,
}
impl ::protobuf::Enum for EthereumERC7730ContainerPath {
@@ -2648,6 +2650,7 @@ impl ::protobuf::Enum for EthereumERC7730ContainerPath {
match value {
1 => ::std::option::Option::Some(EthereumERC7730ContainerPath::FROM),
2 => ::std::option::Option::Some(EthereumERC7730ContainerPath::VALUE),
+ 3 => ::std::option::Option::Some(EthereumERC7730ContainerPath::TO),
_ => ::std::option::Option::None
}
}
@@ -2656,6 +2659,7 @@ impl ::protobuf::Enum for EthereumERC7730ContainerPath {
match str {
"FROM" => ::std::option::Option::Some(EthereumERC7730ContainerPath::FROM),
"VALUE" => ::std::option::Option::Some(EthereumERC7730ContainerPath::VALUE),
+ "TO" => ::std::option::Option::Some(EthereumERC7730ContainerPath::TO),
_ => ::std::option::Option::None
}
}
@@ -2663,6 +2667,7 @@ impl ::protobuf::Enum for EthereumERC7730ContainerPath {
const VALUES: &'static [EthereumERC7730ContainerPath] = &[
EthereumERC7730ContainerPath::FROM,
EthereumERC7730ContainerPath::VALUE,
+ EthereumERC7730ContainerPath::TO,
];
}
@@ -2676,6 +2681,7 @@ impl ::protobuf::EnumFull for EthereumERC7730ContainerPath {
let index = match self {
EthereumERC7730ContainerPath::FROM => 0,
EthereumERC7730ContainerPath::VALUE => 1,
+ EthereumERC7730ContainerPath::TO => 2,
};
Self::enum_descriptor().value_by_index(index)
}
@@ -2747,9 +2753,9 @@ static file_descriptor_proto_data: &'static [u8] = b"\
_STRING\x10\x15*\x85\x01\n!EthereumERC7730FieldFormatterType\x12\x1a\n\
\x16FORMATTER_ADDRESS_NAME\x10\0\x12\x14\n\x10FORMATTER_AMOUNT\x10\x01\
\x12\x1a\n\x16FORMATTER_TOKEN_AMOUNT\x10\x02\x12\x12\n\x0eFORMATTER_UNIT\
- \x10\x03*3\n\x1cEthereumERC7730ContainerPath\x12\x08\n\x04FROM\x10\x01\
- \x12\t\n\x05VALUE\x10\x02B?\n#com.satoshilabs.trezor.lib.protobufB\x18Tr\
- ezorMessageDefinitions\
+ \x10\x03*;\n\x1cEthereumERC7730ContainerPath\x12\x08\n\x04FROM\x10\x01\
+ \x12\t\n\x05VALUE\x10\x02\x12\x06\n\x02TO\x10\x03B?\n#com.satoshilabs.tr\
+ ezor.lib.protobufB\x18TrezorMessageDefinitions\
";
/// `FileDescriptorProto` object which was a source for this generated file
Why this scored 27/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.