feat(tron): Add support for `VoteWitnessContract` - core
What changed, and why it matters
This commit adds support for a new TRON blockchain operation called VoteWitnessContract to Trezor hardware wallets. It lets users vote for up to 9 TRON network validators (called Super Representatives) directly from the device, with on-screen confirmation before signing. There is no indication this fixes a security bug; it is a feature addition.
Treat as a routine feature commit. Reviewers may optionally verify that the 9-vote limit matches protocol constraints and that the UI correctly renders all vote entries without truncation or misleading layout, but no immediate security response is indicated.
Security signals we found
New contract type added to TRON signing path
Input validation: vote list length capped at 9
User confirmation required via hold-to-approve UI
No changelog entry marked with [no changelog]
No vendor security advisory or CVE references present
Evidence from the diff
The patch wires up TronVoteWitnessContract handling in the Trezor Core firmware and Python client library. It adds the message type to the allowed contract list, decodes raw contract bytes into a typed message, enforces a maximum of 9 vote entries, and displays each candidate address plus vote count through the device’s confirmation UI across four supported layout themes (bolt, caesar, delizia, eckhart). The signing flow requires the user to hold-to-confirm, so the operation cannot proceed silently.
Changed components
core/src/apps/tron/sign_tx.pycore/src/apps/tron/layout.pycore/src/apps/tron/consts.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__.pypython/src/trezorlib/tron.pyInspect captured patch +97 / −8
diff --git a/core/src/apps/tron/README.md b/core/src/apps/tron/README.md
index 1963fed7..a5da791f 100644
--- a/core/src/apps/tron/README.md
+++ b/core/src/apps/tron/README.md
@@ -32,7 +32,7 @@ Therefore, we do not show any fees to the user on the device. The host applicati
- [X] `FreezeBalanceV2`: Stake `TRX` to get more `Bandwidth` or `Energy`
- [X] `UnfreezeBalanceV2`: Unstake frozen `TRX`
- [X] `WithdrawExpireUnfreeze`: Claim Unstaked `TRX` past their lock-up period
-- [ ] `VoteWitnessAccount`: Vote using earned TRON Power. **Coming soon**
+- [X] `VoteWitnessContract`: Vote using earned TRON Power.
### Notes
diff --git a/core/src/apps/tron/consts.py b/core/src/apps/tron/consts.py
index ed5e44d3..47130d20 100644
--- a/core/src/apps/tron/consts.py
+++ b/core/src/apps/tron/consts.py
@@ -6,15 +6,11 @@ if TYPE_CHECKING:
from buffer_types import AnyBytes
from typing import Iterator, Tuple
- from trezor.messages import TronTransferContract
-
- TronMessageType = TronTransferContract
-
TYPE_URL_TEMPLATE = "type.googleapis.com/protocol."
-# TODO: Use TypeVar like ethereum/keychain.py:MsgInSignTx
CONTRACT_TYPES = (
MessageType.TronTransferContract,
+ MessageType.TronVoteWitnessContract,
MessageType.TronTriggerSmartContract,
MessageType.TronFreezeBalanceV2Contract,
MessageType.TronUnfreezeBalanceV2Contract,
diff --git a/core/src/apps/tron/layout.py b/core/src/apps/tron/layout.py
index 30056b52..597d458b 100644
--- a/core/src/apps/tron/layout.py
+++ b/core/src/apps/tron/layout.py
@@ -9,7 +9,11 @@ from .helpers import get_encoded_address
if TYPE_CHECKING:
from buffer_types import AnyBytes
- from trezor.messages import TronTransferContract, TronTriggerSmartContract
+ from trezor.messages import (
+ TronTransferContract,
+ TronTriggerSmartContract,
+ TronVoteWitnessContract,
+ )
def format_trx_amount(amount: int) -> str:
@@ -154,3 +158,12 @@ async def confirm_withdraw_unfreeze(owner_address: AnyBytes) -> None:
br_name="tron/claim",
cancel=True,
)
+
+
+async def confirm_votes(contract: TronVoteWitnessContract) -> None:
+ from trezor.ui.layouts import confirm_tron_voting
+
+ voting_list: list[tuple[int, str]] = [
+ (vote.count, get_encoded_address(vote.address)) for vote in contract.votes
+ ]
+ await confirm_tron_voting(voting_list)
diff --git a/core/src/apps/tron/sign_tx.py b/core/src/apps/tron/sign_tx.py
index 7e649fdf..30745bea 100644
--- a/core/src/apps/tron/sign_tx.py
+++ b/core/src/apps/tron/sign_tx.py
@@ -89,11 +89,12 @@ async def process_contract(
# But it causes type error in messages.TronRawContract.type.
from trezor import TR
from trezor.enums import TronRawContractType
- from trezor.ui.layouts import confirm_tron_send
_INT64_MAX = const(9_223_372_036_854_775_807)
if messages.TronTransferContract.is_type_of(contract):
+ from trezor.ui.layouts import confirm_tron_send
+
contract_type = TronRawContractType.TransferContract
await layout.confirm_transfer_contract(contract)
if contract.amount > _INT64_MAX:
@@ -148,6 +149,11 @@ async def process_contract(
contract_type = TronRawContractType.WithdrawExpireUnfreezeContract
await layout.confirm_withdraw_unfreeze(contract.owner_address)
+ elif messages.TronVoteWitnessContract.is_type_of(contract):
+ if len(contract.votes) > 9:
+ raise DataError("Tron: too many votes")
+ contract_type = TronRawContractType.VoteWitnessContract
+ await layout.confirm_votes(contract)
else:
raise DataError("Tron: contract type unknown")
diff --git a/core/src/trezor/ui/layouts/bolt/__init__.py b/core/src/trezor/ui/layouts/bolt/__init__.py
index 93a94825..994c9218 100644
--- a/core/src/trezor/ui/layouts/bolt/__init__.py
+++ b/core/src/trezor/ui/layouts/bolt/__init__.py
@@ -1609,6 +1609,20 @@ if not utils.BITCOIN_ONLY:
None,
)
+ async def confirm_tron_voting(voting_list: list[tuple[int, str]]) -> None:
+ await raise_if_not_confirmed(
+ trezorui_api.confirm_properties(
+ title=TR.words__review,
+ items=[
+ (f"{TR.words__votes}: {vote[0]}", f"{vote[1]}\n", True)
+ for vote in voting_list
+ ],
+ hold=True,
+ ),
+ br_name="tron/vote",
+ br_code=ButtonRequestType.SignTx,
+ )
+
def confirm_joint_total(spending_amount: str, total_amount: str) -> Awaitable[None]:
return raise_if_not_confirmed(
diff --git a/core/src/trezor/ui/layouts/caesar/__init__.py b/core/src/trezor/ui/layouts/caesar/__init__.py
index 6308a170..2b4a206a 100644
--- a/core/src/trezor/ui/layouts/caesar/__init__.py
+++ b/core/src/trezor/ui/layouts/caesar/__init__.py
@@ -1672,6 +1672,21 @@ if not utils.BITCOIN_ONLY:
br_name=br_name,
)
+ async def confirm_tron_voting(voting_list: list[tuple[int, str]]) -> None:
+ await raise_if_not_confirmed(
+ trezorui_api.confirm_properties(
+ title=TR.words__review,
+ subtitle=TR.words__voting,
+ items=[
+ (f"{TR.words__votes}: {vote[0]}", vote[1], True)
+ for vote in voting_list
+ ],
+ hold=True,
+ ),
+ br_name="tron/vote",
+ br_code=ButtonRequestType.SignTx,
+ )
+
def confirm_joint_total(spending_amount: str, total_amount: str) -> Awaitable[None]:
return confirm_properties(
diff --git a/core/src/trezor/ui/layouts/delizia/__init__.py b/core/src/trezor/ui/layouts/delizia/__init__.py
index 4d7a97dd..b1f9a535 100644
--- a/core/src/trezor/ui/layouts/delizia/__init__.py
+++ b/core/src/trezor/ui/layouts/delizia/__init__.py
@@ -1565,6 +1565,24 @@ if not utils.BITCOIN_ONLY:
None,
)
+ async def confirm_tron_voting(voting_list: list[tuple[int, str]]) -> None:
+
+ item_list: list[StrPropertyType] = []
+ for vote_count, address in voting_list:
+ item_list.append((TR.words__address, address, True))
+ item_list.append((f"\n{TR.words__votes}", f"{vote_count}", False))
+
+ await raise_if_not_confirmed(
+ trezorui_api.confirm_properties(
+ title=TR.words__review,
+ subtitle=TR.words__voting,
+ items=item_list,
+ hold=True,
+ ),
+ br_name="tron/vote",
+ br_code=ButtonRequestType.SignTx,
+ )
+
def confirm_joint_total(spending_amount: str, total_amount: str) -> Awaitable[None]:
return _confirm_summary(
diff --git a/core/src/trezor/ui/layouts/eckhart/__init__.py b/core/src/trezor/ui/layouts/eckhart/__init__.py
index 95bdeaaf..434fd54e 100644
--- a/core/src/trezor/ui/layouts/eckhart/__init__.py
+++ b/core/src/trezor/ui/layouts/eckhart/__init__.py
@@ -1662,6 +1662,24 @@ if not utils.BITCOIN_ONLY:
None,
)
+ async def confirm_tron_voting(voting_list: list[tuple[int, str]]) -> None:
+
+ item_list: list[StrPropertyType] = []
+ for vote_count, address in voting_list:
+ item_list.append((TR.words__address, address, True))
+ item_list.append((f"\n{TR.words__votes}", f"{vote_count}", False))
+
+ await raise_if_not_confirmed(
+ trezorui_api.confirm_properties(
+ title=TR.words__review,
+ subtitle=TR.words__voting,
+ items=item_list,
+ hold=True,
+ ),
+ br_name="tron/vote",
+ br_code=ButtonRequestType.SignTx,
+ )
+
def confirm_joint_total(spending_amount: str, total_amount: str) -> Awaitable[None]:
return _confirm_summary(
diff --git a/python/src/trezorlib/tron.py b/python/src/trezorlib/tron.py
index 256fc5f9..eb42957b 100644
--- a/python/src/trezorlib/tron.py
+++ b/python/src/trezorlib/tron.py
@@ -14,6 +14,7 @@ if TYPE_CHECKING:
messages.TronFreezeBalanceV2Contract,
messages.TronUnfreezeBalanceV2Contract,
messages.TronWithdrawUnfreeze,
+ messages.TronVoteWitnessContract,
]
DEFAULT_BIP32_PATH = "m/44h/195h/0h/0/0"
@@ -86,6 +87,14 @@ def from_raw_data(
contract = messages.TronWithdrawUnfreeze(
owner_address=raw_contract.owner_address,
)
+ elif contract_type == messages.TronRawContractType.VoteWitnessContract:
+ raw_contract = load_message(
+ io.BytesIO(parameter_value),
+ messages.TronVoteWitnessContract,
+ )
+ contract = messages.TronVoteWitnessContract(
+ owner_address=raw_contract.owner_address, votes=raw_contract.votes
+ )
else:
raise ValueError(f"Unsupported contract type: {contract_type}")
Why this scored 22/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.