feat(clear_signing): identify a tokenAmount token by a constant address
What changed, and why it matters
This commit adds a new optional field to Trezor's Ethereum clear-signing protocol that lets a token amount be tied to a fixed contract address rather than pulled from transaction data. It is a feature addition, not a bug fix, and there is no evidence in the commit that it addresses a security flaw. The change does not appear to introduce an obvious vulnerability, but it slightly expands the attack surface by adding another way token addresses are resolved.
Treat as a routine feature commit. If auditing, verify that `const_token_address` values are validated (e.g., 20-byte Ethereum address length and checksum where applicable) before being used in token metadata lookups, and ensure the new field cannot be combined with `token_path` in a way that causes confusion or address spoofing in UI display.
Security signals we found
New optional protobuf field for token address resolution
Token address now accepted as literal constant without visible length/format validation
No changelog entry provided
No vendor security disclosure or CVE references present
Evidence from the diff
The change introduces EthereumERC7730FieldInfo.const_token_address, an optional bytes protobuf field, and updates TokenAmountFormatter to accept either a token_path (resolved via path_walker) or a literal const_token_address. If both are absent, it raises InvalidFormatDefinition. The field is additive and optional, so it should not break existing descriptors. No validation of the address length or format is added in the visible diff. The commit is marked [no changelog] and is co-authored by an Anthropic model.
Changed components
core/src/apps/ethereum/clear_signing.pycommon/protob/messages-definitions.protocore/src/trezor/messages.pypython/src/trezorlib/messages.pyrust/trezor-client/src/protos/generated/messages_definitions.rsInspect captured patch +83 / −6
diff --git a/common/protob/messages-definitions.proto b/common/protob/messages-definitions.proto
index 2359c77b..1ada61c9 100644
--- a/common/protob/messages-definitions.proto
+++ b/common/protob/messages-definitions.proto
@@ -178,6 +178,10 @@ message EthereumERC7730FieldInfo {
optional uint32 decimals = 6;
optional string base = 7;
optional bool prefix = 8;
+
+ // Constant token address, specific to the contract.
+ // Typically a metadata value, resolved by the parser.
+ optional bytes const_token_address = 9;
}
/**
diff --git a/core/src/apps/ethereum/clear_signing.py b/core/src/apps/ethereum/clear_signing.py
index 1ca60eb3..b1fa8966 100644
--- a/core/src/apps/ethereum/clear_signing.py
+++ b/core/src/apps/ethereum/clear_signing.py
@@ -295,11 +295,13 @@ class AmountFormatter(FieldFormatter):
class TokenAmountFormatter(FieldFormatter):
def __init__(
self,
- token_path: Path,
+ token_path: Path | None = None,
+ const_token_address: bytes | None = None,
native_currency_address: list[bytes] | None = None,
threshold: int | None = None,
) -> None:
self.token_path = token_path
+ self.const_token_address = const_token_address
self.native_currency_address = native_currency_address
self.threshold = threshold
@@ -320,8 +322,15 @@ class TokenAmountFormatter(FieldFormatter):
if not isinstance(amount, int):
raise InvalidFormatDefinition
- token_address = path_walker(self.token_path)
- if not isinstance(token_address, bytes):
+ if self.const_token_address is not None:
+ # token given as a literal constant address
+ token_address = self.const_token_address
+ elif self.token_path is not None:
+ walked = path_walker(self.token_path)
+ if not isinstance(walked, bytes):
+ raise InvalidFormatDefinition
+ token_address = walked
+ else:
raise InvalidFormatDefinition
if self.native_currency_address is not None:
@@ -686,6 +695,10 @@ class FieldDefinition:
formatter_params = {}
if info.token_path is not None:
formatter_params["token_path"] = decode_path(info.token_path)
+ if info.const_token_address is not None:
+ formatter_params["const_token_address"] = bytes(
+ info.const_token_address
+ )
if info.threshold is not None:
formatter_params["threshold"] = int.from_bytes(info.threshold, "big")
formatter = TokenAmountFormatter(**formatter_params)
diff --git a/core/src/trezor/messages.py b/core/src/trezor/messages.py
index 0cc24f35..b791d320 100644
--- a/core/src/trezor/messages.py
+++ b/core/src/trezor/messages.py
@@ -3391,6 +3391,7 @@ if TYPE_CHECKING:
decimals: "int | None"
base: "str | None"
prefix: "bool | None"
+ const_token_address: "AnyBytes | None"
def __init__(
self,
@@ -3403,6 +3404,7 @@ if TYPE_CHECKING:
decimals: "int | None" = None,
base: "str | None" = None,
prefix: "bool | None" = None,
+ const_token_address: "AnyBytes | None" = None,
) -> None:
pass
diff --git a/python/src/trezorlib/messages.py b/python/src/trezorlib/messages.py
index ad6daf63..1f42efd3 100644
--- a/python/src/trezorlib/messages.py
+++ b/python/src/trezorlib/messages.py
@@ -4808,6 +4808,7 @@ class EthereumERC7730FieldInfo(protobuf.MessageType):
6: protobuf.Field("decimals", "uint32", repeated=False, required=False, default=None),
7: protobuf.Field("base", "string", repeated=False, required=False, default=None),
8: protobuf.Field("prefix", "bool", repeated=False, required=False, default=None),
+ 9: protobuf.Field("const_token_address", "bytes", repeated=False, required=False, default=None),
}
def __init__(
@@ -4821,6 +4822,7 @@ class EthereumERC7730FieldInfo(protobuf.MessageType):
decimals: Optional["int"] = None,
base: Optional["str"] = None,
prefix: Optional["bool"] = None,
+ const_token_address: Optional["bytes"] = None,
) -> None:
self.path = path
self.label = label
@@ -4830,6 +4832,7 @@ class EthereumERC7730FieldInfo(protobuf.MessageType):
self.decimals = decimals
self.base = base
self.prefix = prefix
+ self.const_token_address = const_token_address
class EthereumDisplayFormatInfo(protobuf.MessageType):
diff --git a/rust/trezor-client/src/protos/generated/messages_definitions.rs b/rust/trezor-client/src/protos/generated/messages_definitions.rs
index b444b51c..838752f9 100644
--- a/rust/trezor-client/src/protos/generated/messages_definitions.rs
+++ b/rust/trezor-client/src/protos/generated/messages_definitions.rs
@@ -1537,6 +1537,8 @@ pub struct EthereumERC7730FieldInfo {
pub base: ::std::option::Option<::std::string::String>,
// @@protoc_insertion_point(field:hw.trezor.messages.definitions.EthereumERC7730FieldInfo.prefix)
pub prefix: ::std::option::Option<bool>,
+ // @@protoc_insertion_point(field:hw.trezor.messages.definitions.EthereumERC7730FieldInfo.const_token_address)
+ pub const_token_address: ::std::option::Option<::std::vec::Vec<u8>>,
// special fields
// @@protoc_insertion_point(special_field:hw.trezor.messages.definitions.EthereumERC7730FieldInfo.special_fields)
pub special_fields: ::protobuf::SpecialFields,
@@ -1721,8 +1723,44 @@ impl EthereumERC7730FieldInfo {
self.prefix = ::std::option::Option::Some(v);
}
+ // optional bytes const_token_address = 9;
+
+ pub fn const_token_address(&self) -> &[u8] {
+ match self.const_token_address.as_ref() {
+ Some(v) => v,
+ None => &[],
+ }
+ }
+
+ pub fn clear_const_token_address(&mut self) {
+ self.const_token_address = ::std::option::Option::None;
+ }
+
+ pub fn has_const_token_address(&self) -> bool {
+ self.const_token_address.is_some()
+ }
+
+ // Param is passed by value, moved
+ pub fn set_const_token_address(&mut self, v: ::std::vec::Vec<u8>) {
+ self.const_token_address = ::std::option::Option::Some(v);
+ }
+
+ // Mutable pointer to the field.
+ // If field is not initialized, it is initialized with default value first.
+ pub fn mut_const_token_address(&mut self) -> &mut ::std::vec::Vec<u8> {
+ if self.const_token_address.is_none() {
+ self.const_token_address = ::std::option::Option::Some(::std::vec::Vec::new());
+ }
+ self.const_token_address.as_mut().unwrap()
+ }
+
+ // Take field
+ pub fn take_const_token_address(&mut self) -> ::std::vec::Vec<u8> {
+ self.const_token_address.take().unwrap_or_else(|| ::std::vec::Vec::new())
+ }
+
fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData {
- let mut fields = ::std::vec::Vec::with_capacity(8);
+ let mut fields = ::std::vec::Vec::with_capacity(9);
let mut oneofs = ::std::vec::Vec::with_capacity(0);
fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, EthereumERC7730Path>(
"path",
@@ -1764,6 +1802,11 @@ impl EthereumERC7730FieldInfo {
|m: &EthereumERC7730FieldInfo| { &m.prefix },
|m: &mut EthereumERC7730FieldInfo| { &mut m.prefix },
));
+ fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>(
+ "const_token_address",
+ |m: &EthereumERC7730FieldInfo| { &m.const_token_address },
+ |m: &mut EthereumERC7730FieldInfo| { &mut m.const_token_address },
+ ));
::protobuf::reflect::GeneratedMessageDescriptorData::new_2::<EthereumERC7730FieldInfo>(
"EthereumERC7730FieldInfo",
fields,
@@ -1825,6 +1868,9 @@ impl ::protobuf::Message for EthereumERC7730FieldInfo {
64 => {
self.prefix = ::std::option::Option::Some(is.read_bool()?);
},
+ 74 => {
+ self.const_token_address = ::std::option::Option::Some(is.read_bytes()?);
+ },
tag => {
::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?;
},
@@ -1863,6 +1909,9 @@ impl ::protobuf::Message for EthereumERC7730FieldInfo {
if let Some(v) = self.prefix {
my_size += 1 + 1;
}
+ if let Some(v) = self.const_token_address.as_ref() {
+ my_size += ::protobuf::rt::bytes_size(9, &v);
+ }
my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields());
self.special_fields.cached_size().set(my_size as u32);
my_size
@@ -1893,6 +1942,9 @@ impl ::protobuf::Message for EthereumERC7730FieldInfo {
if let Some(v) = self.prefix {
os.write_bool(8, v)?;
}
+ if let Some(v) = self.const_token_address.as_ref() {
+ os.write_bytes(9, v)?;
+ }
os.write_unknown_fields(self.special_fields.unknown_fields())?;
::std::result::Result::Ok(())
}
@@ -1918,6 +1970,7 @@ impl ::protobuf::Message for EthereumERC7730FieldInfo {
self.decimals = ::std::option::Option::None;
self.base = ::std::option::Option::None;
self.prefix = ::std::option::Option::None;
+ self.const_token_address = ::std::option::Option::None;
self.special_fields.clear();
}
@@ -1931,6 +1984,7 @@ impl ::protobuf::Message for EthereumERC7730FieldInfo {
decimals: ::std::option::Option::None,
base: ::std::option::Option::None,
prefix: ::std::option::Option::None,
+ const_token_address: ::std::option::Option::None,
special_fields: ::protobuf::SpecialFields::new(),
};
&instance
@@ -2757,7 +2811,7 @@ static file_descriptor_proto_data: &'static [u8] = b"\
ons.EthereumABIValueInfoR\x05array\"\x8e\x01\n\x13EthereumERC7730Path\
\x12\x12\n\x04path\x18\x01\x20\x03(\x11R\x04path\x12c\n\x0econtainer_pat\
h\x18\x02\x20\x01(\x0e2<.hw.trezor.messages.definitions.EthereumERC7730C\
- ontainerPathR\rcontainerPath\"\x94\x03\n\x18EthereumERC7730FieldInfo\x12\
+ ontainerPathR\rcontainerPath\"\xc4\x03\n\x18EthereumERC7730FieldInfo\x12\
G\n\x04path\x18\x01\x20\x02(\x0b23.hw.trezor.messages.definitions.Ethere\
umERC7730PathR\x04path\x12\x14\n\x05label\x18\x02\x20\x02(\tR\x05label\
\x12_\n\tformatter\x18\x03\x20\x02(\x0e2A.hw.trezor.messages.definitions\
@@ -2766,7 +2820,8 @@ static file_descriptor_proto_data: &'static [u8] = b"\
\ttokenPath\x12\x1c\n\tthreshold\x18\x05\x20\x01(\x0cR\tthreshold\x12\
\x1a\n\x08decimals\x18\x06\x20\x01(\rR\x08decimals\x12\x12\n\x04base\x18\
\x07\x20\x01(\tR\x04base\x12\x16\n\x06prefix\x18\x08\x20\x01(\x08R\x06pr\
- efix\"\xd5\x02\n\x19EthereumDisplayFormatInfo\x12\x19\n\x08chain_id\x18\
+ efix\x12.\n\x13const_token_address\x18\t\x20\x01(\x0cR\x11constTokenAddr\
+ ess\"\xd5\x02\n\x19EthereumDisplayFormatInfo\x12\x19\n\x08chain_id\x18\
\x01\x20\x02(\x04R\x07chainId\x12\x18\n\x07address\x18\x02\x20\x02(\x0cR\
\x07address\x12\x19\n\x08func_sig\x18\x03\x20\x02(\x0cR\x07funcSig\x12\
\x16\n\x06intent\x18\x04\x20\x02(\tR\x06intent\x12i\n\x15parameter_defin\
Why this scored 19/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.