refactor: prepare for ERC-7730 clear signing
What changed, and why it matters
This commit is a code refactor that reorganizes how Trezor handles Ethereum transaction display logic. It splits existing code into new modules for 'clear signing' and staking, and changes how token amounts and addresses are formatted before being shown to the user. There is no direct evidence in the commit that this fixes a security vulnerability; it appears to be preparation for supporting a new Ethereum standard (ERC-7730). However, because it touches the code that decides what users see on screen before signing transactions, any bug introduced here could affect how clearly users understand what they are approving.
Treat this as a routine refactor with potential UI-parity implications. Reviewers should verify that the new clear_signing parser correctly rejects malformed function calls, that unknown-token warnings are shown consistently for both approve and transfer flows, and that no confirmation steps were accidentally dropped during the move. No urgent security action is indicated by the diff alone.
Security signals we found
Refactor of transaction confirmation UI logic for Ethereum token transfers and approvals
New unknown-token confirmation path added for transfer transactions
Amount formatting and address parsing logic moved between modules
Staking and clear-signing logic separated into new modules
No explicit security fix or vulnerability description in commit message
Evidence from the diff
The commit refactors Ethereum transaction confirmation flows in trezor-firmware. It introduces clear_signing.py and clear_signing_constants.py, moves staking logic into staking.py, and updates sign_tx.py and layout.py to use the new structure. The refactor changes the signature of require_confirm_approve and require_confirm_tx, moving token amount formatting and unknown-token handling earlier in the flow. It also adds handling for unknown tokens in the transfer flow, which previously did not show the unknown-token warning. The commit is titled as preparation for ERC-7730 clear signing and includes [no changelog].
Changed components
core/src/apps/ethereum/sign_tx.pycore/src/apps/ethereum/layout.pycore/src/apps/ethereum/clear_signing.pycore/src/apps/ethereum/clear_signing_constants.pycore/src/apps/ethereum/staking.pycore/src/apps/ethereum/sc_constants.pyInspect captured patch +727 / −377
diff --git a/core/embed/upymod/qstrdefsport.h b/core/embed/upymod/qstrdefsport.h
index fbbb62cc..d4d0f5eb 100644
--- a/core/embed/upymod/qstrdefsport.h
+++ b/core/embed/upymod/qstrdefsport.h
@@ -542,6 +542,8 @@ Q(apps.eos.layout)
Q(apps.eos.sign_tx)
Q(apps.eos.writers)
Q(apps.ethereum)
+Q(apps.ethereum.clear_signing)
+Q(apps.ethereum.clear_signing_constants)
Q(apps.ethereum.definitions)
Q(apps.ethereum.get_address)
Q(apps.ethereum.get_public_key)
@@ -554,6 +556,7 @@ Q(apps.ethereum.sign_message)
Q(apps.ethereum.sign_tx)
Q(apps.ethereum.sign_tx_eip1559)
Q(apps.ethereum.sign_typed_data)
+Q(apps.ethereum.staking)
Q(apps.ethereum.tokens)
Q(apps.ethereum.verify_message)
Q(apps.monero)
@@ -692,6 +695,8 @@ Q(cardano)
Q(certificates)
Q(chacha_poly)
Q(chunks)
+Q(clear_signing)
+Q(clear_signing_constants)
Q(clsag)
Q(common)
Q(constants)
@@ -764,6 +769,7 @@ Q(sign_typed_data)
Q(signer)
Q(signing)
Q(solana)
+Q(staking)
Q(state)
Q(stellar)
Q(step_01_init_transaction)
diff --git a/core/src/apps/ethereum/clear_signing.py b/core/src/apps/ethereum/clear_signing.py
new file mode 100644
index 00000000..225f69ee
--- /dev/null
+++ b/core/src/apps/ethereum/clear_signing.py
@@ -0,0 +1,371 @@
+from micropython import const
+from typing import TYPE_CHECKING
+
+from trezor import TR
+from trezor.crypto import base58
+from trezor.utils import BufferReader
+
+from apps.ethereum import clear_signing_constants as constants
+
+from .helpers import address_from_bytes, format_ethereum_amount
+
+if TYPE_CHECKING:
+ from typing import Any, Callable, Coroutine, Iterable
+
+ from trezor.messages import EthereumNetworkInfo, EthereumTokenInfo
+ from trezor.ui.layouts import StrPropertyType
+
+ from .definitions import Definitions
+ from .keychain import MsgInSignTx
+
+ Value = int | bytes | None
+ FieldParser = Callable[[memoryview], Value]
+ FieldFormatter = Callable[
+ [Value, EthereumNetworkInfo, EthereumTokenInfo], str | None
+ ]
+
+
+class InvalidFunctionCall(Exception):
+ pass
+
+
+# field types - can be any Solidity type - currently just address and uint256
+
+
+def parse_address(arg: memoryview) -> Value:
+ from .sc_constants import SC_ARGUMENT_ADDRESS_BYTES, SC_ARGUMENT_BYTES
+
+ if any(byte != 0 for byte in arg[: SC_ARGUMENT_BYTES - SC_ARGUMENT_ADDRESS_BYTES]):
+ raise InvalidFunctionCall
+
+ return bytes(arg[SC_ARGUMENT_BYTES - SC_ARGUMENT_ADDRESS_BYTES :])
+
+
+def parse_uint256(arg: memoryview) -> Value:
+ return int.from_bytes(arg, "big")
+
+
+# field formatters: https://eips.ethereum.org/EIPS/eip-7730#field-formats
+
+
+def format_address_name(
+ address: Value, network: EthereumNetworkInfo, _token: EthereumTokenInfo
+) -> str | None:
+ if address is None:
+ return None
+ else:
+ assert isinstance(address, bytes)
+ return address_from_bytes(address, network)
+
+
+def get_token_amount_formatter(threshold: int | None = None) -> FieldFormatter:
+ def format_token_amount(
+ amount: Value, network: EthereumNetworkInfo, token: EthereumTokenInfo
+ ) -> str | None:
+ if amount is None:
+ return None
+ else:
+ assert isinstance(amount, int)
+ if threshold is not None and amount > threshold:
+ # 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".
+ return None
+ return format_ethereum_amount(amount, token, network)
+
+ return format_token_amount
+
+
+# https://eips.ethereum.org/EIPS/eip-7730#context-section
+
+
+class BindingContext:
+ def __init__(self, deployments: Iterable[tuple[int, bytes]]) -> None:
+ self.deployments = deployments
+
+ def matches(self, chain_id: int, address: bytes) -> bool:
+ for d_chain_id, d_address in self.deployments:
+ if d_chain_id == chain_id and d_address == address:
+ return True
+ return False
+
+
+# https://eips.ethereum.org/EIPS/eip-7730#structured-data-format-specification
+
+
+class Field:
+ def __init__(
+ self,
+ label: str | None,
+ parser: FieldParser,
+ formatter: FieldFormatter,
+ ) -> None:
+ self.label = label
+ self.parser = parser
+ self.formatter = formatter
+
+
+class DisplayFormat:
+ def __init__(
+ self,
+ binding_context: BindingContext | None,
+ func_sig: bytes,
+ intent: str,
+ interpolated_intent: str | None,
+ fields: list[Field],
+ ) -> None:
+ self.binding_context = binding_context
+ self.func_sig = func_sig
+ self.intent = intent
+ self.interpolated_intent = interpolated_intent
+ self.fields = fields
+
+ def parse_fields(
+ self,
+ data_reader: BufferReader,
+ network: EthereumNetworkInfo,
+ token: EthereumTokenInfo,
+ ) -> Iterable[tuple[Value, StrPropertyType]]:
+ from .sc_constants import SC_ARGUMENT_BYTES
+
+ for field in self.fields:
+ if data_reader.remaining_count() < SC_ARGUMENT_BYTES:
+ raise InvalidFunctionCall
+ arg = data_reader.read_memoryview(SC_ARGUMENT_BYTES)
+ value = field.parser(arg)
+ yield (
+ value,
+ (
+ field.label,
+ field.formatter(value, network, token),
+ None,
+ ),
+ )
+ if data_reader.remaining_count() > 0:
+ raise InvalidFunctionCall
+
+ def matches_context(self, chain_id: int, address: bytes) -> bool:
+ if self.binding_context is None:
+ return True
+
+ return self.binding_context.matches(chain_id, address)
+
+
+def get_approver(
+ msg: MsgInSignTx,
+ definitions: Definitions,
+ address_bytes: bytes,
+ value: int,
+ maximum_fee: str,
+ fee_items: Iterable[StrPropertyType],
+) -> Coroutine[Any, Any, None] | None:
+ from .sc_constants import SC_FUNC_SIG_BYTES
+
+ # local_cache_attribute
+ network = definitions.network
+ chain_id = msg.chain_id
+
+ if not address_bytes or value != 0:
+ return None
+
+ # only parse the initial chunk for now
+ if msg.data_length != len(msg.data_initial_chunk):
+ return None
+
+ data_reader = BufferReader(msg.data_initial_chunk)
+ if data_reader.remaining_count() < SC_FUNC_SIG_BYTES:
+ return None
+
+ token = definitions.get_token(address_bytes)
+
+ func_sig = data_reader.read_memoryview(SC_FUNC_SIG_BYTES)
+
+ display_format = None
+ for f in ALL_DISPLAY_FORMATS:
+ if f.func_sig == func_sig:
+ display_format = f
+ break
+ else:
+ return None
+
+ if not display_format.matches_context(chain_id, address_bytes):
+ return None
+
+ try:
+ args = list(display_format.parse_fields(data_reader, network, token))
+ except InvalidFunctionCall:
+ return None
+
+ # custom treatment of certain functions (APPROVE, TRANSFER)
+
+ if func_sig == APPROVE_DISPLAY_FORMAT.func_sig:
+ assert len(args) == 2
+
+ (arg0_raw_value, (arg0_name, arg0_formatted_value, _)) = args[0]
+ assert arg0_name == "Spender"
+ assert isinstance(arg0_raw_value, bytes)
+ assert isinstance(arg0_formatted_value, str)
+
+ (arg1_raw_value, (arg1_name, arg1_formatted_value, _)) = args[1]
+ assert arg1_name == "Amount"
+ assert isinstance(arg1_raw_value, int)
+
+ return _get_approve_handler(
+ arg0_formatted_value,
+ constants.KNOWN_ADDRESSES.get(arg0_raw_value),
+ arg1_formatted_value,
+ arg1_raw_value == SC_FUNC_APPROVE_REVOKE_AMOUNT,
+ address_bytes,
+ msg,
+ network,
+ token,
+ maximum_fee,
+ fee_items,
+ )
+ elif func_sig == TRANSFER_DISPLAY_FORMAT.func_sig:
+ assert len(args) == 2
+ (_, (arg0_name, arg0_formatted_value, _)) = args[0]
+ assert arg0_name == "To"
+ assert isinstance(arg0_formatted_value, str)
+
+ (_, (arg1_name, arg1_formatted_value, _)) = args[1]
+ assert arg1_name == "Amount"
+ assert isinstance(arg1_formatted_value, str)
+
+ return _get_transfer_handler(
+ arg0_formatted_value,
+ arg1_formatted_value,
+ address_bytes,
+ msg,
+ token,
+ maximum_fee,
+ fee_items,
+ )
+
+ # generic UI for any function that has a `DisplayFormat`
+
+ return _handle_generic_ui(display_format, args, address_bytes, token)
+
+
+def _get_approve_handler(
+ recipient_addr: str,
+ recipient_str: str | None,
+ value: str | None,
+ is_revoke: bool,
+ address_bytes: bytes,
+ msg: MsgInSignTx,
+ network: EthereumNetworkInfo,
+ token: EthereumTokenInfo,
+ maximum_fee: str,
+ fee_items: Iterable[StrPropertyType],
+) -> Coroutine[Any, Any, None] | None:
+ from .layout import require_confirm_approve
+
+ return require_confirm_approve(
+ recipient_addr,
+ value,
+ recipient_str,
+ msg.address_n,
+ maximum_fee,
+ fee_items,
+ msg.chain_id,
+ network,
+ token,
+ address_bytes,
+ is_revoke,
+ bool(msg.chunkify),
+ )
+
+
+def _get_transfer_handler(
+ recipient_addr: str,
+ value: str,
+ address_bytes: bytes,
+ msg: MsgInSignTx,
+ token: EthereumTokenInfo,
+ maximum_fee: str,
+ fee_items: Iterable[StrPropertyType],
+) -> Coroutine[Any, Any, None] | None:
+ from .layout import require_confirm_tx
+
+ return require_confirm_tx(
+ recipient_addr,
+ value,
+ address_bytes,
+ msg.address_n,
+ maximum_fee,
+ fee_items,
+ token,
+ is_send=True,
+ chunkify=bool(msg.chunkify),
+ )
+
+
+async def _handle_generic_ui(
+ f: DisplayFormat,
+ args: list[tuple[Value, StrPropertyType]],
+ address_bytes: bytes,
+ token: EthereumTokenInfo,
+) -> None:
+ from trezor.ui.layouts import (
+ confirm_action,
+ confirm_properties,
+ ethereum_address_title,
+ )
+
+ from . import tokens
+ from .layout import require_confirm_address, require_confirm_unknown_token
+
+ if token is tokens.UNKNOWN_TOKEN:
+ title = ethereum_address_title()
+ await require_confirm_unknown_token(title)
+ await require_confirm_address(
+ address_bytes,
+ title,
+ TR.ethereum__token_contract,
+ TR.buttons__continue,
+ "unknown_token",
+ TR.ethereum__unknown_contract_address,
+ )
+
+ await confirm_action("confirm_contract", "Intent", f.intent)
+ await confirm_properties(
+ "confirm_contract",
+ "Confirm contract",
+ (field_display for (_, field_display) in args),
+ )
+
+
+# https://github.com/LedgerHQ/clear-signing-erc7730-registry/blob/master/ercs/calldata-erc20-tokens.json#L27
+
+APPROVE_DISPLAY_FORMAT = DisplayFormat(
+ binding_context=None,
+ func_sig=base58.keccak_32(b"approve(address,uint256)"),
+ intent="Approve",
+ interpolated_intent=None,
+ fields=[
+ Field("Spender", parse_address, format_address_name), # _spender
+ Field(
+ "Amount",
+ parse_uint256,
+ get_token_amount_formatter(
+ threshold=0x8000000000000000000000000000000000000000000000000000000000000000
+ ), # _value
+ ),
+ ],
+)
+SC_FUNC_APPROVE_REVOKE_AMOUNT = const(0)
+
+TRANSFER_DISPLAY_FORMAT = DisplayFormat(
+ binding_context=None,
+ func_sig=base58.keccak_32(b"transfer(address,uint256)"),
+ intent="Send",
+ interpolated_intent=None,
+ fields=[
+ Field("To", parse_address, format_address_name), # _to
+ Field("Amount", parse_uint256, get_token_amount_formatter()), # _value
+ ],
+)
+
+ALL_DISPLAY_FORMATS = [APPROVE_DISPLAY_FORMAT, TRANSFER_DISPLAY_FORMAT]
diff --git a/core/src/apps/ethereum/clear_signing_constants.py b/core/src/apps/ethereum/clear_signing_constants.py
new file mode 100644
index 00000000..177c4018
--- /dev/null
+++ b/core/src/apps/ethereum/clear_signing_constants.py
@@ -0,0 +1,70 @@
+from ubinascii import unhexlify
+
+# https://github.com/LedgerHQ/clear-signing-erc7730-registry/blob/master/registry/1inch/calldata-AggregationRouterV6.json#L9
+ONEINCH_ADDRESS = unhexlify("111111125421cA6dc452d289314280a0f8842A65")
+ONEINCH_CHAINS = [
+ 1,
+ 10,
+ 56,
+ 100,
+ 137,
+ 146,
+ 250,
+ 8217,
+ 8453,
+ 42161,
+ 43114,
+ 59144,
+ 1313161554,
+]
+ONEINCH_OWNER = "1inch Aggregation Router V6"
+
+# https://github.com/LedgerHQ/clear-signing-erc7730-registry/blob/master/registry/lifi/calldata-LIFIDiamond.json#L6
+LIFI_ADDRESS = unhexlify("1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE")
+LIFI_CHAINS = [
+ 1,
+ 10,
+ 25,
+ 56,
+ 100,
+ 106,
+ 122,
+ 137,
+ 204,
+ 250,
+ 252,
+ 288,
+ 324,
+ 1088,
+ 1284,
+ 1285,
+ 5000,
+ 8453,
+ 9001,
+ 34443,
+ 42161,
+ 42170,
+ 42220,
+ 43114,
+ 59144,
+ 81457,
+ 167004,
+ 534352,
+ 1313161554,
+ 1666600000,
+]
+LIFI_OWNER = "LiFI Diamond"
+
+# https://etherscan.io/address/0xe592427a0aece92de3edee1f18e0157c05861564
+UNISWAP_V3_ROUTER_ADDRESS = unhexlify("e592427a0aece92de3edee1f18e0157c05861564")
+# https://github.com/LedgerHQ/clear-signing-erc7730-registry/blob/master/registry/uniswap/calldata-UniswapV3Router02.json#L6
+UNISWAP_V3_ROUTER_02_ADDRESS = unhexlify("68b3465833fb72A70ecDF485E0e4C7bD8665Fc45")
+UNISWAP_V3_ROUTER_CHAINS = [1]
+UNISWAP_OWNER = "Uniswap V3 Router"
+
+KNOWN_ADDRESSES = {
+ ONEINCH_ADDRESS: ONEINCH_OWNER,
+ LIFI_ADDRESS: LIFI_OWNER,
+ UNISWAP_V3_ROUTER_ADDRESS: UNISWAP_OWNER,
+ UNISWAP_V3_ROUTER_02_ADDRESS: UNISWAP_OWNER,
+}
diff --git a/core/src/apps/ethereum/layout.py b/core/src/apps/ethereum/layout.py
index f473f2c3..33fc60a2 100644
--- a/core/src/apps/ethereum/layout.py
+++ b/core/src/apps/ethereum/layout.py
@@ -31,8 +31,9 @@ if TYPE_CHECKING:
async def require_confirm_approve(
- to_bytes: AnyBytes,
- value: int | None,
+ recipient_addr: str,
+ total_amount: str | None,
+ recipient_str: str | None,
address_n: list[int],
maximum_fee: str,
fee_info_items: Iterable[StrPropertyType],
@@ -40,29 +41,26 @@ async def require_confirm_approve(
network: EthereumNetworkInfo,
token: EthereumTokenInfo,
token_address: AnyBytes,
+ is_revoke: bool,
chunkify: bool,
) -> None:
from trezor.ui.layouts import confirm_ethereum_approve
- from apps.ethereum.sc_constants import APPROVE_KNOWN_ADDRESSES as KNOWN_ADDRESSES
- from apps.ethereum.sc_constants import (
- SC_FUNC_APPROVE_REVOKE_AMOUNT as REVOKE_AMOUNT,
- )
-
from . import networks, tokens
- if to_bytes in KNOWN_ADDRESSES:
- recipient_str = KNOWN_ADDRESSES[to_bytes]
- else:
- recipient_str = None
- recipient_addr = address_from_bytes(to_bytes, network)
chain_id_str = f"{chain_id} ({hex(chain_id)})"
token_address_str = address_from_bytes(token_address, network)
- total_amount = (
- format_ethereum_amount(value, token, network) if value is not None else None
- )
account, account_path = get_account_and_path(address_n)
+ if token is tokens.UNKNOWN_TOKEN:
+ title = (
+ TR.ethereum__approve_intro_title_revoke
+ if is_revoke
+ else TR.ethereum__approve_intro_title
+ )
+
+ await require_confirm_unknown_token(title)
+
await confirm_ethereum_approve(
recipient_addr,
recipient_str,
@@ -72,7 +70,7 @@ async def require_confirm_approve(
network is networks.UNKNOWN_NETWORK,
chain_id_str,
network.name,
- value == REVOKE_AMOUNT,
+ is_revoke,
total_amount,
account,
account_path,
@@ -84,20 +82,33 @@ async def require_confirm_approve(
async def require_confirm_tx(
recipient: str | None,
- value: int,
+ total_amount: str,
+ address_bytes: bytes,
address_n: list[int],
maximum_fee: str,
fee_info_items: Iterable[StrPropertyType],
- network: EthereumNetworkInfo,
token: EthereumTokenInfo | None,
is_send: bool,
chunkify: bool,
) -> None:
- from trezor.ui.layouts import confirm_ethereum_tx
+ from trezor.ui.layouts import confirm_ethereum_tx, ethereum_address_title
+
+ from . import tokens
- total_amount = format_ethereum_amount(value, token, network)
account, account_path = get_account_and_path(address_n)
+ if token is tokens.UNKNOWN_TOKEN:
+ title = ethereum_address_title()
+ await require_confirm_unknown_token(title)
+ await require_confirm_address(
+ address_bytes,
+ title,
+ TR.ethereum__token_contract,
+ TR.buttons__continue,
+ "unknown_token",
+ TR.ethereum__unknown_contract_address,
+ )
+
await confirm_ethereum_tx(
recipient,
total_amount,
@@ -119,7 +130,7 @@ async def require_confirm_payment_request(
chain_id: int,
network: EthereumNetworkInfo,
token: EthereumTokenInfo | None,
- token_address: str,
+ token_address: str | None,
) -> None:
from trezor import wire
from trezor.ui.layouts import confirm_payment_request
@@ -182,7 +193,7 @@ async def require_confirm_payment_request(
account_items,
maximum_fee,
fee_info_items,
- [(TR.ethereum__token_contract, token_address)],
+ [(TR.ethereum__token_contract, token_address)] if token_address else [],
)
@@ -271,7 +282,7 @@ async def require_confirm_claim(
)
-async def require_confirm_unknown_token(title: str | None) -> None:
+async def require_confirm_unknown_token(title: str) -> None:
from trezor.ui.layouts import confirm_ethereum_unknown_contract_warning
await confirm_ethereum_unknown_contract_warning(title)
diff --git a/core/src/apps/ethereum/sc_constants.py b/core/src/apps/ethereum/sc_constants.py
index e21e3b27..24574e3c 100644
--- a/core/src/apps/ethereum/sc_constants.py
+++ b/core/src/apps/ethereum/sc_constants.py
@@ -1,57 +1,7 @@
from micropython import const
-from ubinascii import unhexlify
# smart contract 'data' field lengths in bytes
SC_FUNC_SIG_BYTES = const(4)
SC_ARGUMENT_BYTES = const(32)
SC_ARGUMENT_ADDRESS_BYTES = const(20)
-SC_FUNC_APPROVE_REVOKE_AMOUNT = const(0)
-
assert SC_ARGUMENT_ADDRESS_BYTES <= SC_ARGUMENT_BYTES
-
-# Known ERC-20 functions
-
-SC_FUNC_SIG_TRANSFER = unhexlify("a9059cbb")
-SC_FUNC_SIG_APPROVE = unhexlify("095ea7b3")
-SC_FUNC_SIG_STAKE = unhexlify("3a29dbae")
-SC_FUNC_SIG_UNSTAKE = unhexlify("76ec871c")
-SC_FUNC_SIG_CLAIM = unhexlify("33986ffa")
-
-# EIP-7702
-
-EIP_7702_TX_TYPE = const(4)
-EIP_7702_KNOWN_ADDRESSES = {
- unhexlify("000000009B1D0aF20D8C6d0A44e162d11F9b8f00"): "Uniswap",
- unhexlify("69007702764179f14F51cdce752f4f775d74E139"): "alchemyplatform",
- unhexlify("5A7FC11397E9a8AD41BF10bf13F22B0a63f96f6d"): "AmbireTech",
- unhexlify("63c0c19a282a1b52b07dd5a65b58948a07dae32b"): "MetaMask",
- unhexlify(
- "4Cd241E8d1510e30b2076397afc7508Ae59C66c9"
- ): "Ethereum Foundation AA team",
- unhexlify("17c11FDdADac2b341F2455aFe988fec4c3ba26e3"): "Luganodes",
-}
-
-
-# Everstake staking
-
-# addresses for pool (stake/unstake) and accounting (claim) operations
-ADDRESSES_POOL = (
- unhexlify("AFA848357154a6a624686b348303EF9a13F63264"), # Hoodi testnet
- unhexlify("D523794C879D9eC028960a231F866758e405bE34"), # mainnet
-)
-ADDRESSES_ACCOUNTING = (
- unhexlify("624087DD1904ab122A32878Ce9e933C7071F53B9"), # Hoodi testnet
- unhexlify("7a7f0b3c23C23a31cFcb0c44709be70d4D545c6e"), # mainnet
-)
-
-# Approve known addresses
-# This should eventually grow into a more comprehensive database and stored in some other way,
-# but for now let's just keep a few known addresses here!
-
-APPROVE_KNOWN_ADDRESSES = {
- unhexlify("e592427a0aece92de3edee1f18e0157c05861564"): "Uniswap V3 Router",
- unhexlify(
- "111111125421cA6dc452d289314280a0f8842A65"
- ): "1inch Aggregation Router V6",
- unhexlify("1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE"): "LiFI Diamond",
-}
diff --git a/core/src/apps/ethereum/sign_tx.py b/core/src/apps/ethereum/sign_tx.py
index 3d619ac4..a5cf4c5e 100644
--- a/core/src/apps/ethereum/sign_tx.py
+++ b/core/src/apps/ethereum/sign_tx.py
@@ -1,13 +1,12 @@
+from micropython import const
from typing import TYPE_CHECKING
+from ubinascii import unhexlify
from trezor import TR
from trezor.crypto import rlp
from trezor.messages import EthereumTxRequest
-from trezor.utils import BufferReader
from trezor.wire import DataError
-from apps.ethereum import sc_constants as constants
-
from .helpers import address_from_bytes, bytes_from_address
from .keychain import with_keychain_from_chain_id
@@ -15,12 +14,7 @@ if TYPE_CHECKING:
from buffer_types import AnyBytes
from typing import Any, Awaitable, Callable, Coroutine, Iterable
- from trezor.messages import (
- EthereumNetworkInfo,
- EthereumSignTx,
- EthereumTokenInfo,
- EthereumTxAck,
- )
+ from trezor.messages import EthereumSignTx, EthereumTxAck
from trezor.ui.layouts import StrPropertyType
from apps.common.keychain import Keychain
@@ -37,6 +31,20 @@ if TYPE_CHECKING:
# the full value: v = 2 * chain_id + 35 + v_bit
MAX_CHAIN_ID = (0xFFFF_FFFF - 36) // 2
+# EIP-7702
+
+EIP_7702_TX_TYPE = const(4)
+EIP_7702_KNOWN_ADDRESSES = {
+ unhexlify("000000009B1D0aF20D8C6d0A44e162d11F9b8f00"): "Uniswap",
+ unhexlify("69007702764179f14F51cdce752f4f775d74E139"): "alchemyplatform",
+ unhexlify("5A7FC11397E9a8AD41BF10bf13F22B0a63f96f6d"): "AmbireTech",
+ unhexlify("63c0c19a282a1b52b07dd5a65b58948a07dae32b"): "MetaMask",
+ unhexlify(
+ "4Cd241E8d1510e30b2076397afc7508Ae59C66c9"
+ ): "Ethereum Foundation AA team",
+ unhexlify("17c11FDdADac2b341F2455aFe988fec4c3ba26e3"): "Luganodes",
+}
+
@with_keychain_from_chain_id
async def sign_tx(
@@ -52,20 +60,22 @@ async def sign_tx(
from .helpers import format_ethereum_amount, get_fee_items_regular
- data_total = msg.data_length # local_cache_attribute
- tx_type = msg.tx_type # local_cache_attribute
+ # local_cache_attribute
+ data_total = msg.data_length
+ tx_type = msg.tx_type
+ network = defs.network
check_common_fields(msg)
address_bytes = bytes_from_address(msg.to)
- valid_tx_types = (1, 6, constants.EIP_7702_TX_TYPE, None)
+ valid_tx_types = (1, 6, EIP_7702_TX_TYPE, None)
if tx_type not in valid_tx_types:
raise DataError("tx_type out of bounds")
- if tx_type == constants.EIP_7702_TX_TYPE:
+ if tx_type == EIP_7702_TX_TYPE:
if safety_checks.is_strict():
raise DataError("EIP-7702 not allowed in strict checks")
- if address_bytes not in constants.EIP_7702_KNOWN_ADDRESSES:
+ if address_bytes not in EIP_7702_KNOWN_ADDRESSES:
raise DataError("Unknown EIP-7702 address")
if len(msg.gas_price) + len(msg.gas_limit) > 30:
raise DataError("Fee overflow")
@@ -74,11 +84,11 @@ async def sign_tx(
await paths.validate_path(keychain, msg.address_n)
gas_price = int.from_bytes(msg.gas_price, "big")
gas_limit = int.from_bytes(msg.gas_limit, "big")
- maximum_fee = format_ethereum_amount(gas_price * gas_limit, None, defs.network)
+ maximum_fee = format_ethereum_amount(gas_price * gas_limit, None, network)
fee_items = get_fee_items_regular(
gas_price,
gas_limit,
- defs.network,
+ network,
)
payment_req_verifier = None
@@ -138,6 +148,7 @@ async def sign_tx(
# show tx summary and confirm
await confirm_summary
+
# transaction data confirmed, proceed with signing
result = _sign_digest(msg, keychain, digest)
@@ -225,31 +236,24 @@ async def confirm_tx_data(
maximum_fee: str,
fee_items: Iterable[StrPropertyType],
data_total_len: int,
- payment_req_verifier: PaymentRequestVerifier | None,
+ payment_request_verifier: PaymentRequestVerifier | None,
) -> tuple[ConfirmDataFn, Coroutine[Any, Any, None]]:
"""Returns data chunk callback and transaction summary layout to be awaited."""
+ from trezor.ui.layouts import confirm_value
- from trezor.ui.layouts import confirm_value, ethereum_address_title
-
- from . import tokens
- from .layout import (
- require_confirm_address,
- require_confirm_approve,
- require_confirm_payment_request,
- require_confirm_tx,
- require_confirm_unknown_token,
- )
+ from . import clear_signing, staking
+ from .helpers import format_ethereum_amount
+ from .layout import require_confirm_payment_request, require_confirm_tx
# local_cache_attribute
- payment_req = msg.payment_req
- SC_FUNC_SIG_APPROVE = constants.SC_FUNC_SIG_APPROVE
- REVOKE_AMOUNT = constants.SC_FUNC_APPROVE_REVOKE_AMOUNT
- EIP_7702_TX_TYPE = constants.EIP_7702_TX_TYPE
+ network = defs.network
- staking_approver = get_staking_approver(
- msg, defs.network, address_bytes, maximum_fee, fee_items
+ staking_approver = staking.get_approver(
+ msg, network, address_bytes, maximum_fee, fee_items
)
if staking_approver is not None:
+ if payment_request_verifier is not None:
+ raise DataError("Payment Requests don't support staking")
return make_progress(data_total_len), staking_approver
if tx_type == EIP_7702_TX_TYPE:
@@ -257,209 +261,68 @@ async def confirm_tx_data(
# as part of the initial validation
await confirm_value(
TR.ethereum__eip_7702_title,
- constants.EIP_7702_KNOWN_ADDRESSES[address_bytes],
+ EIP_7702_KNOWN_ADDRESSES[address_bytes],
TR.ethereum__eip_7702,
"confirm_provider",
)
- token, token_address, func_sig, recipient, value = (
- await _handle_known_contract_calls(msg, defs, address_bytes)
+ value = int.from_bytes(msg.value, "big")
+
+ clear_signing_approver = clear_signing.get_approver(
+ msg, defs, address_bytes, value, maximum_fee, fee_items
)
+ if clear_signing_approver is not None:
+ if payment_request_verifier is not None:
+ raise DataError("Payment Requests don't support contract interactions")
+ return make_progress(data_total_len), clear_signing_approver
- if token is tokens.UNKNOWN_TOKEN:
- if func_sig == SC_FUNC_SIG_APPROVE:
- if value == REVOKE_AMOUNT:
- title = TR.ethereum__approve_intro_title_revoke
- else:
- title = TR.ethereum__approve_intro_title
- else:
- title = ethereum_address_title()
- await require_confirm_unknown_token(title)
- if func_sig != SC_FUNC_SIG_APPROVE:
- # For unknown tokens we also show the token address immediately after the warning
- # except in the case of the "approve" flow which shows the token address later on!
- await require_confirm_address(
- address_bytes,
- ethereum_address_title(),
- TR.ethereum__token_contract,
- TR.buttons__continue,
- "unknown_token",
- TR.ethereum__unknown_contract_address,
- )
+ recipient_str = (
+ address_from_bytes(address_bytes, network) if address_bytes else None
+ )
- if func_sig == SC_FUNC_SIG_APPROVE:
- assert token
- assert token_address
+ if payment_request_verifier is not None:
+ if data_total_len > 0:
+ raise DataError("Payment Requests don't support contract interactions")
- if payment_req_verifier is not None:
- raise DataError("Payment Requests not supported for the APPROVE call")
+ # If a payment_request_verifier is provided, then msg.payment_req must have been set.
+ assert msg.payment_req is not None
+ assert recipient_str is not None
- return make_progress(data_total_len), require_confirm_approve(
- recipient,
- value,
+ payment_request_verifier.add_output(value, recipient_str or "")
+ payment_request_verifier.verify()
+ return make_progress(data_total_len), require_confirm_payment_request(
+ recipient_str,
+ msg.payment_req,
msg.address_n,
maximum_fee,
fee_items,
msg.chain_id,
- defs.network,
- token,
- token_address,
- chunkify=bool(msg.chunkify),
+ network,
+ # TODO: SLIP-24 cannot deal with tokens? So we should get rid of these?
+ None,
+ None,
)
else:
- assert value is not None
-
- recipient_str = (
- address_from_bytes(recipient, defs.network) if recipient else None
- )
- token_address_str = address_from_bytes(address_bytes, defs.network)
-
- is_contract_interaction = token is None and data_total_len > 0
-
- if payment_req_verifier is not None:
- if is_contract_interaction:
- raise DataError("Payment Requests don't support contract interactions")
-
- # If a payment_req_verifier is provided, then msg.payment_req must have been set.
- assert payment_req is not None
- assert recipient_str is not None
- payment_req_verifier.add_output(value, recipient_str or "")
- payment_req_verifier.verify()
- return make_progress(data_total_len), require_confirm_payment_request(
- recipient_str,
- payment_req,
- msg.address_n,
- maximum_fee,
- fee_items,
- msg.chain_id,
- defs.network,
- token,
- token_address_str,
- )
+ if data_total_len > 0:
+ # blind signing: we have data but `clear_signing` did not recognize the function
+ confirm_data_chunk = make_confirm_data(data_total_len)
else:
- if is_contract_interaction:
- confirm_data_chunk = make_confirm_data(data_total_len)
- else:
- confirm_data_chunk = make_progress(data_total_len)
-
- return confirm_data_chunk, require_confirm_tx(
- recipient_str,
- value,
- msg.address_n,
- maximum_fee,
- fee_items,
- defs.network,
- token,
- is_send=not is_contract_interaction and tx_type != EIP_7702_TX_TYPE,
- chunkify=bool(msg.chunkify),
- )
-
-
-def get_staking_approver(
- msg: MsgInSignTx,
- network: EthereumNetworkInfo,
- address_bytes: bytes,
- maximum_fee: str,
- fee_items: Iterable[StrPropertyType],
-) -> Coroutine[Any, Any, None] | None:
- """
- Returns a awaitable confirmation for ETH staking approval.
-
- `None` is returned for non-staking related transactions.
- """
-
- data_reader = BufferReader(msg.data_initial_chunk)
- if data_reader.remaining_count() < constants.SC_FUNC_SIG_BYTES:
- return None
-
- func_sig = data_reader.read_memoryview(constants.SC_FUNC_SIG_BYTES)
- if address_bytes in constants.ADDRESSES_POOL:
- if func_sig == constants.SC_FUNC_SIG_STAKE:
- return _handle_staking_tx_stake(
- data_reader, msg, network, address_bytes, maximum_fee, fee_items
- )
- if func_sig == constants.SC_FUNC_SIG_UNSTAKE:
- return _handle_staking_tx_unstake(
- data_reader, msg, network, address_bytes, maximum_fee, fee_items
- )
-
- if address_bytes in constants.ADDRESSES_ACCOUNTING:
- if func_sig == constants.SC_FUNC_SIG_CLAIM:
- return _handle_staking_tx_claim(
- data_reader,
- msg,
- address_bytes,
- maximum_fee,
- fee_items,
- network,
- bool(msg.chunkify),
- )
-
- # data not corresponding to staking transaction
- return None
-
-
-async def _handle_known_contract_calls(
- msg: MsgInSignTx,
- definitions: Definitions,
- address_bytes: bytes,
-) -> tuple[
- EthereumTokenInfo | None, AnyBytes | None, AnyBytes | None, AnyBytes, int | None
-]:
- # local_cache_attribute
- data_initial_chunk = msg.data_initial_chunk
- SC_FUNC_SIG_BYTES = constants.SC_FUNC_SIG_BYTES
- SC_ARGUMENT_BYTES = constants.SC_ARGUMENT_BYTES
- SC_ARGUMENT_ADDRESS_BYTES = constants.SC_ARGUMENT_ADDRESS_BYTES
- SC_FUNC_SIG_APPROVE = constants.SC_FUNC_SIG_APPROVE
- SC_FUNC_SIG_TRANSFER = constants.SC_FUNC_SIG_TRANSFER
-
- token = None
- token_address = None
- recipient = address_bytes
- value = int.from_bytes(msg.value, "big")
+ confirm_data_chunk = make_progress(data_total_len)
- data_reader = BufferReader(data_initial_chunk)
- if data_reader.remaining_count() < SC_FUNC_SIG_BYTES:
- return token, token_address, None, recipient, value
- func_sig = data_reader.read_memoryview(SC_FUNC_SIG_BYTES)
-
- if (
- len(msg.to) in (40, 42)
- and len(msg.value) == 0
- and msg.data_length == 68
- and len(data_initial_chunk) == 68
- and func_sig in (SC_FUNC_SIG_TRANSFER, SC_FUNC_SIG_APPROVE)
- ):
- # The two functions happen to have the exact same parameters, so we treat them together.
- # This will need to be made into a more generic solution eventually.
- # arg0: address, Address, 20 bytes (left padded with zeroes)
- # arg1: value, uint256, 32 bytes
-
- if data_reader.remaining_count() < SC_ARGUMENT_BYTES * 2:
- return token, token_address, None, recipient, value
- arg0 = data_reader.read_memoryview(SC_ARGUMENT_BYTES)
- assert all(
- byte == 0 for byte in arg0[: SC_ARGUMENT_BYTES - SC_ARGUMENT_ADDRESS_BYTES]
+ token = (
+ None # what we want to confirm here is the ETH amount being sent on-chain
+ )
+ return confirm_data_chunk, require_confirm_tx(
+ recipient_str,
+ format_ethereum_amount(value, token, network),
+ address_bytes,
+ msg.address_n,
+ maximum_fee,
+ fee_items,
+ token,
+ is_send=(data_total_len == 0 and tx_type != EIP_7702_TX_TYPE),
+ chunkify=bool(msg.chunkify),
)
- recipient = bytes(arg0[SC_ARGUMENT_BYTES - SC_ARGUMENT_ADDRESS_BYTES :])
- arg1 = data_reader.read_memoryview(SC_ARGUMENT_BYTES)
- if func_sig == SC_FUNC_SIG_APPROVE and all(byte == 255 for byte in arg1):
- # "Unlimited" approval (all bits set) is a special case
- # which we encode as value=None internally.
- value = None
- else:
- value = int.from_bytes(arg1, "big")
-
- token = definitions.get_token(address_bytes)
- token_address = address_bytes
- else:
- # If the function was known but something else (data length) was unexpected,
- # pretend we did not recognize the function so we fall back to blind signing.
- # See the approve_avantis test case and ERC-8021.
- func_sig = None
-
- return token, token_address, func_sig, recipient, value
def _get_total_length(msg: EthereumSignTx, data_total: int) -> int:
@@ -541,89 +404,3 @@ def check_common_fields(msg: MsgInSignTx) -> None:
if msg.chain_id == 0:
raise DataError("Chain ID out of bounds")
-
-
-async def _handle_staking_tx_stake(
- data_reader: BufferReader,
- msg: MsgInSignTx,
- network: EthereumNetworkInfo,
- address_bytes: bytes,
- maximum_fee: str,
- fee_items: Iterable[StrPropertyType],
-) -> None:
- from .layout import require_confirm_stake
-
- # stake args:
- # - arg0: uint64, source (1 for Trezor)
- try:
- _ = data_reader.read_memoryview(constants.SC_ARGUMENT_BYTES) # skip arg0
- if data_reader.remaining_count() != 0:
- raise ValueError # wrong number of arguments for stake (should be 1)
- except (ValueError, EOFError):
- raise DataError("Invalid staking transaction call")
-
- await require_confirm_stake(
- address_bytes,
- int.from_bytes(msg.value, "big"),
- msg.address_n,
- maximum_fee,
- fee_items,
- network,
- bool(msg.chunkify),
- )
-
-
-async def _handle_staking_tx_unstake(
- data_reader: BufferReader,
- msg: MsgInSignTx,
- network: EthereumNetworkInfo,
- address_bytes: bytes,
- maximum_fee: str,
- fee_items: Iterable[StrPropertyType],
-) -> None:
- from .layout import require_confirm_unstake
-
- # unstake args:
- # - arg0: uint256, value
- # - arg1: uint16, isAllowedInterchange (bool)
- # - arg2: uint64, source (1 for Trezor)
- try:
- value = int.from_bytes(
- data_reader.read_memoryview(constants.SC_ARGUMENT_BYTES), "big"
- ) # parse arg0
- _ = data_reader.read_memoryview(constants.SC_ARGUMENT_BYTES) # skip arg1
- _ = data_reader.read_memoryview(constants.SC_ARGUMENT_BYTES) # skip arg2
- if data_reader.remaining_count() != 0:
- raise ValueError # wrong number of arguments for unstake (should be 3)
- except (ValueError, EOFError):
- raise DataError("Invalid staking transaction call")
-
- await require_confirm_unstake(
- address_bytes,
- value,
- msg.address_n,
- maximum_fee,
- fee_items,
- network,
- bool(msg.chunkify),
- )
-
-
-async def _handle_staking_tx_claim(
- data_reader: BufferReader,
- msg: MsgInSignTx,
- staking_addr: bytes,
- maximum_fee: str,
- fee_items: Iterable[StrPropertyType],
- network: EthereumNetworkInfo,
- chunkify: bool,
-) -> None:
- from .layout import require_confirm_claim
-
- # claim has no args
- if data_reader.remaining_count() != 0:
- raise DataError("Invalid staking transaction call")
-
- await require_confirm_claim(
- staking_addr, msg.address_n, maximum_fee, fee_items, network, chunkify
- )
diff --git a/core/src/apps/ethereum/staking.py b/core/src/apps/ethereum/staking.py
new file mode 100644
index 00000000..04c03506
--- /dev/null
+++ b/core/src/apps/ethereum/staking.py
@@ -0,0 +1,165 @@
+from typing import TYPE_CHECKING
+from ubinascii import unhexlify
+
+from trezor.utils import BufferReader
+from trezor.wire import DataError
+
+if TYPE_CHECKING:
+ from typing import Any, Coroutine, Iterable
+
+ from trezor.messages import EthereumNetworkInfo
+ from trezor.ui.layouts import StrPropertyType
+
+ from .keychain import MsgInSignTx
+
+
+FUNC_SIG_STAKE = unhexlify("3a29dbae")
+FUNC_SIG_UNSTAKE = unhexlify("76ec871c")
+FUNC_SIG_CLAIM = unhexlify("33986ffa")
+
+# addresses for pool (stake/unstake) and accounting (claim) operations
+ADDRESSES_POOL = (
+ unhexlify("AFA848357154a6a624686b348303EF9a13F63264"), # Hoodi testnet
+ unhexlify("D523794C879D9eC028960a231F866758e405bE34"), # mainnet
+)
+ADDRESSES_ACCOUNTING = (
+ unhexlify("624087DD1904ab122A32878Ce9e933C7071F53B9"), # Hoodi testnet
+ unhexlify("7a7f0b3c23C23a31cFcb0c44709be70d4D545c6e"), # mainnet
+)
+
+
+def get_approver(
+ msg: MsgInSignTx,
+ network: EthereumNetworkInfo,
+ address_bytes: bytes,
+ maximum_fee: str,
+ fee_items: Iterable[StrPropertyType],
+) -> Coroutine[Any, Any, None] | None:
+ """
+ Returns a awaitable confirmation for ETH staking approval.
+
+ `None` is returned for non-staking related transactions.
+ """
+
+ from .sc_constants import SC_FUNC_SIG_BYTES
+
+ if msg.data_length > len(msg.data_initial_chunk):
+ return None
+
+ data_reader = BufferReader(msg.data_initial_chunk)
+ if data_reader.remaining_count() < SC_FUNC_SIG_BYTES:
+ return None
+
+ func_sig = data_reader.read_memoryview(SC_FUNC_SIG_BYTES)
+ if address_bytes in ADDRESSES_POOL:
+ if func_sig == FUNC_SIG_STAKE:
+ return _handle_staking_tx_stake(
+ data_reader, msg, network, address_bytes, maximum_fee, fee_items
+ )
+ if func_sig == FUNC_SIG_UNSTAKE:
+ return _handle_staking_tx_unstake(
+ data_reader, msg, network, address_bytes, maximum_fee, fee_items
+ )
+
+ if address_bytes in ADDRESSES_ACCOUNTING:
+ if func_sig == FUNC_SIG_CLAIM:
+ return _handle_staking_tx_claim(
+ data_reader,
+ msg,
+ address_bytes,
+ maximum_fee,
+ fee_items,
+ network,
+ bool(msg.chunkify),
+ )
+
+ # data not corresponding to staking transaction
+ return None
+
+
+async def _handle_staking_tx_stake(
+ data_reader: BufferReader,
+ msg: MsgInSignTx,
+ network: EthereumNetworkInfo,
+ address_bytes: bytes,
+ maximum_fee: str,
+ fee_items: Iterable[StrPropertyType],
+) -> None:
+ from .layout import require_confirm_stake
+ from .sc_constants import SC_ARGUMENT_BYTES
+
+ # stake args:
+ # - arg0: uint64, source (1 for Trezor)
+ try:
+ _ = data_reader.read_memoryview(SC_ARGUMENT_BYTES) # skip arg0
+ if data_reader.remaining_count() != 0:
+ raise ValueError # wrong number of arguments for stake (should be 1)
+ except (ValueError, EOFError):
+ raise DataError("Invalid staking transaction call")
+
+ await require_confirm_stake(
+ address_bytes,
+ int.from_bytes(msg.value, "big"),
+ msg.address_n,
+ maximum_fee,
+ fee_items,
+ network,
+ bool(msg.chunkify),
+ )
+
+
+async def _handle_staking_tx_unstake(
+ data_reader: BufferReader,
+ msg: MsgInSignTx,
+ network: EthereumNetworkInfo,
+ address_bytes: bytes,
+ maximum_fee: str,
+ fee_items: Iterable[StrPropertyType],
+) -> None:
+ from .layout import require_confirm_unstake
+ from .sc_constants import SC_ARGUMENT_BYTES
+
+ # unstake args:
+ # - arg0: uint256, value
+ # - arg1: uint16, isAllowedInterchange (bool)
+ # - arg2: uint64, source (1 for Trezor)
+ try:
+ value = int.from_bytes(
+ data_reader.read_memoryview(SC_ARGUMENT_BYTES), "big"
+ ) # parse arg0
+ _ = data_reader.read_memoryview(SC_ARGUMENT_BYTES) # skip arg1
+ _ = data_reader.read_memoryview(SC_ARGUMENT_BYTES) # skip arg2
+ if data_reader.remaining_count() != 0:
+ raise ValueError # wrong number of arguments for unstake (should be 3)
+ except (ValueError, EOFError):
+ raise DataError("Invalid staking transaction call")
+
+ await require_confirm_unstake(
+ address_bytes,
+ value,
+ msg.address_n,
+ maximum_fee,
+ fee_items,
+ network,
+ bool(msg.chunkify),
+ )
+
+
+async def _handle_staking_tx_claim(
+ data_reader: BufferReader,
+ msg: MsgInSignTx,
+ staking_addr: bytes,
+ maximum_fee: str,
+ fee_items: Iterable[StrPropertyType],
+ network: EthereumNetworkInfo,
+ chunkify: bool,
+) -> None:
+ from .layout import require_confirm_claim
+
+ # claim has no args
+ if data_reader.remaining_count() != 0:
+ raise DataError("Invalid staking transaction call")
+
+ await require_confirm_claim(
+ staking_addr, msg.address_n, maximum_fee, fee_items, network, chunkify
+ )
Why this scored 34/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.