feat(ethereum): clear signing unlimited amounts
What changed, and why it matters
This commit changes how Trezor displays Ethereum token-approval amounts that exceed a safety threshold. Previously, the code returned 'None' for such large amounts, and the user interface fell back to a generic 'Unlimited amount' warning. Now the formatter explicitly returns a special 'AboveThreshold' marker carrying the text 'Unlimited', and each device UI layout uses that marker to show the same warning and display text. The change is a user-interface refinement, not a fix for a cryptographic bug or a remote exploit. It does, however, make the 'unlimited' signal explicit rather than implicit, which slightly reduces the chance that a future UI change would accidentally treat a huge allowance as an ordinary amount.
Treat as a routine feature/refactor commit. Reviewers may want to verify that all four UI layouts handle the AboveThreshold sentinel consistently and that no code path still interprets a huge allowance as a normal numeric amount. No urgent security response is indicated by the diff alone.
Security signals we found
UI-only change in Ethereum token-approval flow
Explicit sentinel replaces implicit None for above-threshold amounts
No change to signing, parsing, or access-control logic
No changelog entry supplied by vendor
Evidence from the diff
The patch introduces a new sentinel class, trezor.ui.layouts.properties.AboveThreshold, and threads it through Ethereum clear-signing formatting and four device UI layouts (bolt, caesar, delizia, eckhart). TokenAmountFormatter now returns AboveThreshold(TR.words__unlimited) instead of None when amount > threshold. The layout functions check isinstance(total_amount, AboveThreshold) to decide whether to show the unlimited-approval warning and to render total_amount.message in the property list. The change also updates type hints and removes an old TODO about signaling threshold-exceeded amounts. No cryptographic, parsing, or authorization logic is altered.
Changed components
core/src/apps/ethereum/clear_signing.pycore/src/apps/ethereum/layout.pycore/src/trezor/ui/layouts/bolt/__init__.pycore/src/trezor/ui/layouts/caesar/__init__.pycore/src/trezor/ui/layouts/delizia/__init__.pycore/src/trezor/ui/layouts/eckhart/__init__.pycore/src/trezor/ui/layouts/properties.pyInspect captured patch +79 / −30
diff --git a/core/src/apps/ethereum/clear_signing.py b/core/src/apps/ethereum/clear_signing.py
index 46e3fe28..530fc33d 100644
--- a/core/src/apps/ethereum/clear_signing.py
+++ b/core/src/apps/ethereum/clear_signing.py
@@ -21,6 +21,7 @@ if TYPE_CHECKING:
EthereumTokenInfo,
)
from trezor.ui.layouts import StrPropertyType
+ from trezor.ui.layouts.properties import AboveThreshold
from typing_extensions import Self
from apps.common.payment_request import PaymentRequestVerifier
@@ -225,7 +226,7 @@ class FieldFormatter:
msg: MsgInSignTx,
defs: Definitions,
path_walker: PathWalker,
- ) -> tuple[str | None, EthereumTokenInfo | None, AnyBytes | None]:
+ ) -> tuple[str | AboveThreshold | None, EthereumTokenInfo | None, AnyBytes | None]:
"""
Format a field using the current formatter.
Return the formatted value and optionally a token and a token address,
@@ -242,7 +243,7 @@ class AddressNameFormatter(FieldFormatter):
_msg: MsgInSignTx,
defs: Definitions,
_path_walker: PathWalker,
- ) -> tuple[str | None, EthereumTokenInfo | None, AnyBytes | None]:
+ ) -> tuple[str | AboveThreshold | None, EthereumTokenInfo | None, AnyBytes | None]:
if address is None:
return None, None, None
elif isinstance(address, str):
@@ -260,7 +261,7 @@ class AmountFormatter(FieldFormatter):
_msg: MsgInSignTx,
defs: Definitions,
_path_walker: PathWalker,
- ) -> tuple[str | None, EthereumTokenInfo | None, AnyBytes | None]:
+ ) -> tuple[str | AboveThreshold | None, EthereumTokenInfo | None, AnyBytes | None]:
if amount is None:
return None, None, None
else:
@@ -274,11 +275,6 @@ class AmountFormatter(FieldFormatter):
class TokenAmountFormatter(FieldFormatter):
- # TODO: figure out a way for the formatter to signal that the amount was above the threshold.
- # For now we return None and `confirm_ethereum_approve` shows the "Unlimited amount" warning,
- # but the `tokenAmount` spec allows this message to be customized in which case
- # being above the threshold could mean something else, not just "Unlimited".
-
def __init__(
self,
token_path: Path,
@@ -295,7 +291,9 @@ class TokenAmountFormatter(FieldFormatter):
msg: MsgInSignTx,
defs: Definitions,
path_walker: PathWalker,
- ) -> tuple[str | None, EthereumTokenInfo | None, AnyBytes | None]:
+ ) -> tuple[str | AboveThreshold | None, EthereumTokenInfo | None, AnyBytes | None]:
+ from trezor.ui.layouts.properties import AboveThreshold
+
from .tokens import UNKNOWN_TOKEN
if amount is None:
@@ -311,7 +309,7 @@ class TokenAmountFormatter(FieldFormatter):
if self.native_currency_address is not None:
if token_address in self.native_currency_address:
if self.threshold is not None and amount > self.threshold:
- return None, None, None
+ return AboveThreshold(TR.words__unlimited), None, None
else:
return (
format_ethereum_amount(amount, None, defs.network),
@@ -331,7 +329,7 @@ class TokenAmountFormatter(FieldFormatter):
token = received_definitions.get_token(token_address)
if self.threshold is not None and amount > self.threshold:
- return None, token, token_address
+ return AboveThreshold(TR.words__unlimited), token, token_address
else:
return (
format_ethereum_amount(amount, token, defs.network),
@@ -352,7 +350,7 @@ class UnitFormatter(FieldFormatter):
_msg: MsgInSignTx,
_definitions: Definitions,
_path_walker: PathWalker,
- ) -> tuple[str | None, EthereumTokenInfo | None, AnyBytes | None]:
+ ) -> tuple[str | AboveThreshold | None, EthereumTokenInfo | None, AnyBytes | None]:
if value is None:
return None, None, None
else:
@@ -660,7 +658,13 @@ class DisplayFormat:
defs: Definitions,
) -> tuple[
list[AnyValue],
- list[tuple[StrPropertyType, EthereumTokenInfo | None, AnyBytes | None]],
+ list[
+ tuple[
+ tuple[str, str | AboveThreshold | None, bool | None],
+ EthereumTokenInfo | None,
+ AnyBytes | None,
+ ]
+ ],
]:
parameters: list[AnyValue] = []
@@ -718,7 +722,11 @@ class DisplayFormat:
return p
fields: list[
- tuple[StrPropertyType, EthereumTokenInfo | None, AnyBytes | None]
+ tuple[
+ tuple[str, str | AboveThreshold | None, bool | None],
+ EthereumTokenInfo | None,
+ AnyBytes | None,
+ ]
] = []
for field_definition in self.field_definitions:
value = get_value_for_path(field_definition.path)
@@ -994,6 +1002,8 @@ async def _handle_generic_ui(
defs: Definitions,
maximum_fee: str,
) -> None:
+ from trezor.ui.layouts.properties import AboveThreshold
+
from . import tokens
from .helpers import bytes_from_address
from .layout import require_confirm_clear_signing
@@ -1003,8 +1013,10 @@ async def _handle_generic_ui(
properties_to_confirm = []
- for field, actual_token, actual_token_address in fields:
- properties_to_confirm.append(field)
+ for (label, formatted, hint), actual_token, actual_token_address in fields:
+ if isinstance(formatted, AboveThreshold):
+ formatted = formatted.message
+ properties_to_confirm.append((label, formatted, hint))
if actual_token is tokens.UNKNOWN_TOKEN:
assert actual_token_address is not None
token_address_str = address_from_bytes(actual_token_address, defs.network)
diff --git a/core/src/apps/ethereum/layout.py b/core/src/apps/ethereum/layout.py
index 5de794c0..9374f9c4 100644
--- a/core/src/apps/ethereum/layout.py
+++ b/core/src/apps/ethereum/layout.py
@@ -31,11 +31,12 @@ if TYPE_CHECKING:
PaymentRequest,
)
from trezor.ui.layouts import StrPropertyType
+ from trezor.ui.layouts.properties import AboveThreshold
async def require_confirm_approve(
recipient_addr: str,
- total_amount: str | None,
+ total_amount: str | AboveThreshold | None,
recipient_str: str | None,
address_n: list[int],
maximum_fee: str,
diff --git a/core/src/trezor/ui/layouts/bolt/__init__.py b/core/src/trezor/ui/layouts/bolt/__init__.py
index 0b99750b..bff03325 100644
--- a/core/src/trezor/ui/layouts/bolt/__init__.py
+++ b/core/src/trezor/ui/layouts/bolt/__init__.py
@@ -15,6 +15,7 @@ if TYPE_CHECKING:
from trezor.messages import StellarAsset
from ..common import ExceptionType, PropertyType, StrPropertyType
+ from ..properties import AboveThreshold
from ..slip24 import Refund, Trade
@@ -1151,13 +1152,15 @@ if not utils.BITCOIN_ONLY:
chain_id: str,
network_name: str,
is_revoke: bool,
- total_amount: str | None,
+ total_amount: str | AboveThreshold | None,
account: str | None,
account_path: str | None,
maximum_fee: str,
fee_info_items: Iterable[StrPropertyType],
chunkify: bool = False,
) -> None:
+ from ..properties import AboveThreshold
+
await confirm_value(
(
TR.ethereum__approve_intro_title_revoke
@@ -1184,7 +1187,7 @@ if not utils.BITCOIN_ONLY:
chunkify=False if recipient_str else chunkify,
)
- if total_amount is None:
+ if isinstance(total_amount, AboveThreshold):
await show_warning(
"confirm_ethereum_approve",
TR.ethereum__approve_unlimited_template.format(token_symbol),
@@ -1217,7 +1220,11 @@ if not utils.BITCOIN_ONLY:
else [
(
TR.ethereum__approve_amount_allowance,
- total_amount or TR.words__unlimited,
+ (
+ total_amount.message
+ if isinstance(total_amount, AboveThreshold)
+ else total_amount
+ ),
False,
)
]
diff --git a/core/src/trezor/ui/layouts/caesar/__init__.py b/core/src/trezor/ui/layouts/caesar/__init__.py
index 8319fa41..4e48402a 100644
--- a/core/src/trezor/ui/layouts/caesar/__init__.py
+++ b/core/src/trezor/ui/layouts/caesar/__init__.py
@@ -15,6 +15,7 @@ if TYPE_CHECKING:
from ..common import ExceptionType, PropertyType, StrPropertyType
from ..menu import Details
+ from ..properties import AboveThreshold
from ..slip24 import Refund, Trade
@@ -1122,14 +1123,14 @@ if not utils.BITCOIN_ONLY:
chain_id: str,
network_name: str,
is_revoke: bool,
- total_amount: str | None,
+ total_amount: str | AboveThreshold | None,
account: str | None,
account_path: str | None,
maximum_fee: str,
fee_info_items: Iterable[StrPropertyType],
chunkify: bool = False,
) -> None:
- from ..properties import with_colon
+ from ..properties import AboveThreshold, with_colon
await confirm_value(
(
@@ -1159,7 +1160,7 @@ if not utils.BITCOIN_ONLY:
chunkify=False if recipient_str else chunkify,
)
- if total_amount is None:
+ if isinstance(total_amount, AboveThreshold):
await show_warning(
"confirm_ethereum_approve",
TR.ethereum__approve_unlimited_template.format(token_symbol),
@@ -1194,7 +1195,11 @@ if not utils.BITCOIN_ONLY:
else [
(
TR.ethereum__approve_amount_allowance,
- total_amount or TR.words__unlimited,
+ (
+ total_amount.message
+ if isinstance(total_amount, AboveThreshold)
+ else total_amount
+ ),
False,
)
]
diff --git a/core/src/trezor/ui/layouts/delizia/__init__.py b/core/src/trezor/ui/layouts/delizia/__init__.py
index 768eb3c8..a5f58948 100644
--- a/core/src/trezor/ui/layouts/delizia/__init__.py
+++ b/core/src/trezor/ui/layouts/delizia/__init__.py
@@ -21,6 +21,7 @@ if TYPE_CHECKING:
from ..common import ExceptionType, PropertyType, StrPropertyType
from ..menu import Details
+ from ..properties import AboveThreshold
from ..slip24 import Refund, Trade
T = TypeVar("T")
@@ -1124,13 +1125,15 @@ if not utils.BITCOIN_ONLY:
chain_id: str,
network_name: str,
is_revoke: bool,
- total_amount: str | None,
+ total_amount: str | AboveThreshold | None,
account: str | None,
account_path: str | None,
maximum_fee: str,
fee_info_items: Iterable[StrPropertyType],
chunkify: bool = False,
) -> None:
+ from ..properties import AboveThreshold
+
br_name = "confirm_ethereum_approve"
br_code = ButtonRequestType.Other
await confirm_value(
@@ -1175,7 +1178,7 @@ if not utils.BITCOIN_ONLY:
)
await with_info(main_layout, info_layout, br_name, br_code)
- if total_amount is None:
+ if isinstance(total_amount, AboveThreshold):
await show_warning(
br_name,
TR.ethereum__approve_unlimited_template.format(token_symbol),
@@ -1206,7 +1209,11 @@ if not utils.BITCOIN_ONLY:
else [
(
TR.ethereum__approve_amount_allowance,
- total_amount or TR.words__unlimited,
+ (
+ total_amount.message
+ if isinstance(total_amount, AboveThreshold)
+ else total_amount
+ ),
False,
)
]
diff --git a/core/src/trezor/ui/layouts/eckhart/__init__.py b/core/src/trezor/ui/layouts/eckhart/__init__.py
index c337f430..4c01a647 100644
--- a/core/src/trezor/ui/layouts/eckhart/__init__.py
+++ b/core/src/trezor/ui/layouts/eckhart/__init__.py
@@ -21,6 +21,7 @@ if TYPE_CHECKING:
from trezor.ui.layouts.menu import Details
from ..common import ExceptionType, PropertyType, StrPropertyType
+ from ..properties import AboveThreshold
from ..slip24 import Refund, Trade
T = TypeVar("T")
@@ -1148,13 +1149,14 @@ if not utils.BITCOIN_ONLY:
chain_id: str,
network_name: str,
is_revoke: bool,
- total_amount: str | None,
+ total_amount: str | AboveThreshold | None,
account: str | None,
account_path: str | None,
maximum_fee: str,
fee_info_items: Iterable[StrPropertyType],
chunkify: bool = False,
) -> None:
+ from ..properties import AboveThreshold
br_name = "confirm_ethereum_approve"
br_code = ButtonRequestType.Other
@@ -1208,7 +1210,7 @@ if not utils.BITCOIN_ONLY:
)
await with_info(main_layout, info_layout, br_name, br_code)
- if total_amount is None:
+ if isinstance(total_amount, AboveThreshold):
await show_warning(
br_name,
TR.ethereum__approve_unlimited_template.format(token_symbol),
@@ -1239,7 +1241,11 @@ if not utils.BITCOIN_ONLY:
else [
(
TR.ethereum__approve_amount_allowance,
- total_amount or TR.words__unlimited,
+ (
+ total_amount.message
+ if isinstance(total_amount, AboveThreshold)
+ else total_amount
+ ),
False,
)
]
diff --git a/core/src/trezor/ui/layouts/properties.py b/core/src/trezor/ui/layouts/properties.py
index 19238016..f90b5d46 100644
--- a/core/src/trezor/ui/layouts/properties.py
+++ b/core/src/trezor/ui/layouts/properties.py
@@ -13,6 +13,17 @@ if TYPE_CHECKING:
def with_colon(properties: str) -> str: ...
+class AboveThreshold:
+ """Signals that an amount exceeds a threshold.
+
+ Passed to layout functions instead of a plain amount string so they can
+ show the appropriate warning and display text.
+ """
+
+ def __init__(self, message: str) -> None:
+ self.message = message
+
+
def with_colon(
properties: Iterable[StrPropertyType] | str | None = None,
) -> list[StrPropertyType] | str | None:
Why this scored 28/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.