feat(tron): Clear sign some TRC20 transfers.
What changed, and why it matters
This commit expands the Trezor hardware wallet's ability to clearly display details for more Tron TRC20 token transfers on the device screen. Previously, only one test token was recognized; now a built-in list of popular tokens (like USDT, USDD, BTT, etc.) is shown with their proper names and decimal places when a user signs a transfer. This is a user-experience and safety improvement, not a fix for a known exploit.
No immediate security action required. Treat as a normal feature/UX improvement. Reviewers may want to verify the token addresses and decimals against authoritative sources (e.g., TronScan) and consider whether the static list needs a documented update process.
Security signals we found
Improves clear-signing coverage for TRC20 transfers, reducing ambiguity in user confirmations
Removes dependency on EthereumTokenInfo message type for Tron token metadata
Adds a static allow-list of token addresses; tokens not in the list still fall back to unknown-contract confirmation
No input validation changes beyond refactoring existing checks
Evidence from the diff
The patch replaces a single hardcoded testnet token definition in core/src/apps/tron/sign_tx.py with a new token_iterator() in core/src/apps/tron/consts.py that yields 12 TRC20 token address/decimal/symbol tuples. The known-contract handler now matches any of these tokens and passes decimals and symbol directly to the confirmation layout, removing the dependency on EthereumTokenInfo. The parsing logic for transfer(address,uint256) call data is otherwise unchanged. No changelog entry is recorded.
Changed components
core/src/apps/tron/consts.pycore/src/apps/tron/layout.pycore/src/apps/tron/sign_tx.pyInspect captured patch +125 / −53
diff --git a/core/src/apps/tron/consts.py b/core/src/apps/tron/consts.py
index bcf5568d..bb0ff940 100644
--- a/core/src/apps/tron/consts.py
+++ b/core/src/apps/tron/consts.py
@@ -3,6 +3,9 @@ from typing import TYPE_CHECKING
from trezor.enums import MessageType
if TYPE_CHECKING:
+ from buffer_types import AnyBytes
+ from typing import Iterator, Tuple
+
from trezor.messages import TronTransferContract
TronMessageType = TronTransferContract
@@ -27,3 +30,78 @@ def get_contract_type_name(contract_type: int) -> str:
if contract_type in CONTRACT_TYPE_NAMES:
return CONTRACT_TYPE_NAMES[contract_type]
raise ValueError(f"Unknown contract type: {contract_type}")
+
+
+def token_iterator() -> Iterator[Tuple[AnyBytes, int, str]]:
+ # https://shasta.tronscan.org/#/token20/TG3XXyExBkPp9nzdajDZsozEu4BkaSJozs
+ yield (
+ b"\x41\x42\xa1\xe3\x9a\xef\xa4\x92\x90\xf2\xb3\xf9\xed\x68\x8d\x7c\xec\xf8\x6c\xd6\xe0",
+ 6,
+ "tUSDT",
+ )
+ # https://tronscan.org/#/token20/TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t
+ yield (
+ b"\x41\xa6\x14\xf8\x03\xb6\xfd\x78\x09\x86\xa4\x2c\x78\xec\x9c\x7f\x77\xe6\xde\xd1\x3c",
+ 6,
+ "USDT",
+ )
+ # https://tronscan.org/#/token20/TXDk8mbtRbXeYuMNS83CfKPaYYT8XWv9Hz
+ yield (
+ b"\x41\xe9\x1a\x74\x11\xe5\x6c\xe7\x9e\x83\x57\x05\x70\xf4\x9b\x9f\xc3\x5b\x77\x27\xc5",
+ 18,
+ "USDD",
+ )
+ # https://tronscan.org/#/token20/TSSMHYeV2uE9qYH95DqyoCuNCzEL1NvU3S
+ yield (
+ b"\x41\xb4\xa4\x28\xab\x70\x92\xc2\xf1\x39\x5f\x37\x6c\xe2\x97\x03\x3b\x3b\xb4\x46\xc1",
+ 18,
+ "SUN",
+ )
+ # https://tronscan.org/#/token20/TCFLL5dx5ZJdKnWuesXxi1VPwjLVmWZZy9
+ yield (
+ b"\x41\x18\xfd\x06\x26\xda\xf3\xaf\x02\x38\x9a\xef\x3e\xd8\x7d\xb9\xc3\x3f\x63\x8f\xfa",
+ 18,
+ "JST",
+ )
+ # https://tronscan.org/#/token20/TAFjULxiVgT4qWk6UZwjqwZXTSaGaqnVp4
+ yield (
+ b"\x41\x03\x20\x17\x41\x1f\x46\x63\xb3\x17\xfe\x77\xc2\x57\xd2\x8d\x5c\xd1\xb2\x6e\x3d",
+ 18,
+ "BTT",
+ )
+ # https://tronscan.org/#/token20/TLa2f6VPqDgRE67v1736s7bJ8Ray5wYjU7
+ yield (
+ b"\x41\x74\x47\x2e\x7d\x35\x39\x5a\x6b\x5a\xdd\x42\x7e\xec\xb7\xf4\xb6\x2a\xd2\xb0\x71",
+ 6,
+ "WIN",
+ )
+ # https://tronscan.org/#/token20/TYhWwKpw43ENFWBTGpzLHn3882f2au7SMi
+ yield (
+ b"\x41\xf9\x53\x35\xa4\xd4\x2d\xb4\xb7\x0a\x96\x88\xa3\x93\x27\x9f\x2c\x90\xfa\x10\x25",
+ 8,
+ "WBTC",
+ )
+ # https://tronscan.org/#/token20/THb4CqiFdwNHsWsQCs4JhzwjMWys4aqCbF
+ yield (
+ b"\x41\x53\x90\x83\x08\xf4\xaa\x22\x0f\xb1\x0d\x77\x8b\x5d\x1b\x34\x48\x9c\xd6\xed\xfc",
+ 18,
+ "ETH(Tron)",
+ )
+ # https://tronscan.org/#/token20/TPFqcBAaaUMCSVRCqPaQ9QnzKhmuoLR6Rc
+ yield (
+ b"\x41\x91\xbe\xd8\xe7\x84\x24\x9c\x91\x61\x1e\x61\xc4\x58\x5c\x40\xe2\x1f\xd0\xac\xe2",
+ 18,
+ "USD1",
+ )
+ # https://tronscan.org/#/token20/TUPM7K8REVzD2UdV4R5fe5M8XbnR2DdoJ6
+ yield (
+ b"\x41\xca\x03\x03\xe8\xb9\xa7\x38\x12\x17\x77\x11\x6d\xce\xa4\x19\xfe\x52\x4f\x27\x1a",
+ 18,
+ "HTX",
+ )
+ # https://tronscan.org/#/token20/TUpMhErZL2fhh4sVNULAbNKLokS4GjC1F4
+ yield (
+ b"\x41\xce\xbd\xe7\x10\x77\xb8\x30\xb9\x58\xc8\xda\x17\xbc\xdd\xee\xb8\x5d\x0b\xcf\x25",
+ 18,
+ "TUSD",
+ )
diff --git a/core/src/apps/tron/layout.py b/core/src/apps/tron/layout.py
index 80e35f35..0ede03e8 100644
--- a/core/src/apps/tron/layout.py
+++ b/core/src/apps/tron/layout.py
@@ -7,11 +7,8 @@ from trezor import TR, strings
from .helpers import get_encoded_address
if TYPE_CHECKING:
- from trezor.messages import (
- EthereumTokenInfo,
- TronTransferContract,
- TronTriggerSmartContract,
- )
+
+ from trezor.messages import TronTransferContract, TronTriggerSmartContract
def format_trx_amount(amount: int) -> str:
@@ -21,8 +18,8 @@ def format_trx_amount(amount: int) -> str:
return f"{strings.format_amount(amount, _TRX_AMOUNT_DECIMALS)} TRX"
-def format_token_amount(amount: int, token: EthereumTokenInfo) -> str:
- return f"{strings.format_amount(amount, token.decimals)} {token.symbol}"
+def format_token_amount(amount: int, token_decimals: int, token_symbol: str) -> str:
+ return f"{strings.format_amount(amount, token_decimals)} {token_symbol}"
def format_energy_amount(amount: int) -> str:
@@ -76,13 +73,17 @@ async def confirm_unknown_smart_contract(
async def confirm_known_trc20_smart_contract(
- recipient_addr: bytes, amount: int, fee_limit: int, token: EthereumTokenInfo
+ recipient_addr: bytes,
+ amount: int,
+ fee_limit: int,
+ token_decimals: int,
+ token_symbol: str,
) -> None:
from trezor.ui.layouts import confirm_tron_transfer
await confirm_tron_transfer(
recipient_addr=get_encoded_address(recipient_addr),
- total_amount=format_token_amount(amount, token),
+ total_amount=format_token_amount(amount, token_decimals, token_symbol),
maximum_fee=format_energy_amount(fee_limit),
chunkify=True,
)
diff --git a/core/src/apps/tron/sign_tx.py b/core/src/apps/tron/sign_tx.py
index d2b9608a..11651419 100644
--- a/core/src/apps/tron/sign_tx.py
+++ b/core/src/apps/tron/sign_tx.py
@@ -2,7 +2,6 @@ from micropython import const
from typing import TYPE_CHECKING
from trezor import messages
-from trezor.messages import EthereumTokenInfo
from trezor.protobuf import dump_message_buffer
from trezor.wire import DataError
@@ -12,6 +11,7 @@ from . import CURVE, PATTERN, SLIP44_ID, consts, layout
if TYPE_CHECKING:
from buffer_types import AnyBytes
+ from typing import Tuple
from trezor.messages import (
TronRawContract,
@@ -123,7 +123,6 @@ async def process_smart_contract(
await layout.confirm_unknown_smart_contract(contract, fee_limit)
-# TODO: Maybe refactor with ETH. Code duplicated from ethereum/sign_tx.py:_handle_known_contract_calls
async def process_known_trc20_contract(
contract: TronTriggerSmartContract, fee_limit: int
) -> bool:
@@ -137,48 +136,42 @@ async def process_known_trc20_contract(
SC_FUNC_SIG_TRANSFER,
)
- token = get_token_info(contract.contract_address)
- if token and len(contract.data) == 68:
- data_reader = BufferReader(contract.data)
- func_sig = data_reader.read_memoryview(SC_FUNC_SIG_BYTES)
- if func_sig == SC_FUNC_SIG_TRANSFER:
- if data_reader.remaining_count() < SC_ARGUMENT_BYTES * 2:
- return False
- arg0 = data_reader.read_memoryview(SC_ARGUMENT_BYTES)
- assert all(
- byte == 0
- for byte in arg0[: SC_ARGUMENT_BYTES - SC_ARGUMENT_ADDRESS_BYTES]
- )
- # TRON truncates the mandatory prefix \x41 from addresses in data
- recipient = b"\x41" + bytes(
- arg0[SC_ARGUMENT_BYTES - SC_ARGUMENT_ADDRESS_BYTES :]
- )
- arg1 = data_reader.read_memoryview(SC_ARGUMENT_BYTES)
- value = int.from_bytes(arg1, "big")
- else:
- return False
-
- await layout.confirm_known_trc20_smart_contract(
- recipient, value, fee_limit, token
- )
- return True
- else:
+ token_info = get_token_info(contract.contract_address)
+ if token_info is None or len(contract.data) != 68:
return False
+ token_decimals, token_symbol = token_info
-# TODO: Placeholder for actual logic
-def get_token_info(token_address: AnyBytes) -> EthereumTokenInfo | None:
- # Shasta testnet USDT
- if (
- token_address
- == b"\x41\x42\xa1\xe3\x9a\xef\xa4\x92\x90\xf2\xb3\xf9\xed\x68\x8d\x7c\xec\xf8\x6c\xd6\xe0"
- ):
- return EthereumTokenInfo(
- address=b"\x41\x42\xa1\xe3\x9a\xef\xa4\x92\x90\xf2\xb3\xf9\xed\x68\x8d\x7c\xec\xf8\x6c\xd6\xe0",
- chain_id=195,
- decimals=6,
- name="",
- symbol="tUSDT",
- )
- else:
- return None
+ data_reader = BufferReader(contract.data)
+ func_sig = data_reader.read_memoryview(SC_FUNC_SIG_BYTES)
+ if func_sig != SC_FUNC_SIG_TRANSFER:
+ return False
+
+ if data_reader.remaining_count() < SC_ARGUMENT_BYTES * 2:
+ return False
+
+ arg0 = data_reader.read_memoryview(SC_ARGUMENT_BYTES)
+ assert all(
+ byte == 0 for byte in arg0[: SC_ARGUMENT_BYTES - SC_ARGUMENT_ADDRESS_BYTES]
+ )
+ # TRON truncates the mandatory prefix \x41 from addresses in data
+ recipient = b"\x41" + bytes(arg0[SC_ARGUMENT_BYTES - SC_ARGUMENT_ADDRESS_BYTES :])
+
+ arg1 = data_reader.read_memoryview(SC_ARGUMENT_BYTES)
+ value = int.from_bytes(arg1, "big")
+
+ await layout.confirm_known_trc20_smart_contract(
+ recipient,
+ value,
+ fee_limit,
+ token_decimals,
+ token_symbol,
+ )
+ return True
+
+
+def get_token_info(token_address: AnyBytes) -> Tuple[int, str] | None:
+ for address, decimals, symbol in consts.token_iterator():
+ if token_address == address:
+ return decimals, symbol
+ return 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.