fix(solana): ALT recipients shown correctly in system transfer.
What changed, and why it matters
This update fixes how Trezor displays Solana transactions that use Address Lookup Tables (ALTs). Previously, the device could misidentify or fail to show the real recipient when an ALT was involved, which might let an attacker trick a user into approving a transfer to an unexpected address. The patch also blocks ALT references in payment requests and some staking flows where the device cannot safely verify the destination.
Treat as a security-relevant bug fix. Review whether any other Solana instruction handlers still assume direct accounts, and verify the new is_address_reference() helper is applied consistently. Users should update firmware once the release containing this commit is available.
Security signals we found
UI spoofing/misdisplay risk for Solana ALT recipients
Missing validation of ALT references in payment-request and staking paths
Fee-calculation bug: read-only signers not counted
Refactoring replaces inline length checks with typed helper
Evidence from the diff
The commit refactors Solana account handling to use a new is_address_reference() helper instead of checking tuple length inline. It corrects recipient display in system transfers so ALT-referenced recipients are shown via address-reference properties rather than being treated as direct accounts. It also adds guards: payment requests reject ALT-referenced recipients/destinations, and predefined staking/withdrawal confirmations reject ALT references where the device cannot resolve the actual address. Additionally, fee calculation now counts read-only signers (AddressSigReadOnly) as signers.
Changed components
core/src/apps/solana/layout.pycore/src/apps/solana/predefined_transaction.pycore/src/apps/solana/sign_tx.pycore/src/apps/solana/transaction/__init__.pycore/src/apps/solana/types.pyInspect captured patch +73 / −34
diff --git a/core/.changelog.d/248.fixed b/core/.changelog.d/248.fixed
new file mode 100644
index 00000000..0505012d
--- /dev/null
+++ b/core/.changelog.d/248.fixed
@@ -0,0 +1 @@
+Fixed solana ALT recipient and account type parsing.
diff --git a/core/src/apps/solana/layout.py b/core/src/apps/solana/layout.py
index 3dc74641..dc7f411f 100644
--- a/core/src/apps/solana/layout.py
+++ b/core/src/apps/solana/layout.py
@@ -16,7 +16,7 @@ from trezor.ui.layouts import (
from apps.common.paths import address_n_to_str
-from .types import AddressType
+from .types import AddressType, is_address_reference
if TYPE_CHECKING:
from typing import Sequence
@@ -140,8 +140,7 @@ async def confirm_instruction(
continue
account_data: list[PropertyType] = []
- # account included in the transaction directly
- if len(account_value) == 2:
+ if not is_address_reference(account_value):
account_description = f"{base58.encode(account_value[0])}"
token = definitions.get_token(account_value[0])
if token is not None:
@@ -152,14 +151,11 @@ async def confirm_instruction(
account_data.append(
(ui_property.display_name, account_description, True)
)
- # lookup table address reference
- elif len(account_value) == 3:
+ else:
account_data += _get_address_reference_props(
account_value,
ui_property.display_name,
)
- else:
- raise ValueError # Invalid account value
await confirm_properties(
"confirm_instruction",
@@ -173,19 +169,22 @@ async def confirm_instruction(
if instruction.multisig_signers:
signers: list[PropertyType] = []
for i, multisig_signer in enumerate(instruction.multisig_signers, 1):
- multisig_signer_public_key = multisig_signer[0]
-
- path_str = ""
- if multisig_signer_public_key == signer_public_key:
- path_str = f" ({address_n_to_str(signer_path)})"
-
- signers.append(
- (
- f"{TR.words__signer} {i}{path_str}",
- base58.encode(multisig_signer[0]),
- True,
+ if not is_address_reference(multisig_signer):
+ multisig_signer_public_key = multisig_signer[0]
+ path_str = ""
+ if multisig_signer_public_key == signer_public_key:
+ path_str = f" ({address_n_to_str(signer_path)})"
+ signers.append(
+ (
+ f"{TR.words__signer} {i}{path_str}",
+ base58.encode(multisig_signer_public_key),
+ True,
+ )
+ )
+ else:
+ signers += _get_address_reference_props(
+ multisig_signer, f"{TR.words__signer} {i}"
)
- )
await confirm_properties(
"confirm_instruction",
@@ -244,7 +243,7 @@ async def confirm_unsupported_instruction_details(
for i, account in enumerate(instruction.accounts, 1):
accounts: list[PropertyType] = []
- if len(account) == 2:
+ if not is_address_reference(account):
account_public_key = account[0]
address_type = get_address_type(account[1])
@@ -259,13 +258,11 @@ async def confirm_unsupported_instruction_details(
True,
)
)
- elif len(account) == 3:
+ else:
address_type = get_address_type(account[2])
accounts += _get_address_reference_props(
account, f"{TR.words__account} {i} {address_type}"
)
- else:
- raise ValueError # Invalid account value
await confirm_properties(
"accounts",
@@ -312,9 +309,14 @@ async def confirm_system_transfer(
blockhash: bytes,
verified_payment_request: PaymentRequest | None,
) -> None:
+ recipient_account = transfer_instruction.recipient_account
if verified_payment_request:
+ if is_address_reference(recipient_account):
+ raise wire.DataError(
+ "ALT account reference not supported in payment request"
+ )
await confirm_payment_request(
- provider_address=base58.encode(transfer_instruction.recipient_account[0]),
+ provider_address=base58.encode(recipient_account[0]),
address_n=signer_path,
amount=transfer_instruction.lamports,
decimals=9,
@@ -323,11 +325,18 @@ async def confirm_system_transfer(
verified_payment_request=verified_payment_request,
)
else:
- await confirm_solana_recipient(
- recipient=base58.encode(transfer_instruction.recipient_account[0]),
- title=TR.words__recipient,
- items=[(TR.words__blockhash, base58.encode(blockhash), True)],
- )
+ if not is_address_reference(recipient_account):
+ await confirm_solana_recipient(
+ recipient=base58.encode(recipient_account[0]),
+ title=TR.words__recipient,
+ items=[(TR.words__blockhash, base58.encode(blockhash), True)],
+ )
+ else:
+ await confirm_properties(
+ "confirm_recipient",
+ TR.words__recipient,
+ _get_address_reference_props(recipient_account, TR.words__recipient),
+ )
await confirm_custom_transaction(transfer_instruction.lamports, 9, "SOL", fee)
diff --git a/core/src/apps/solana/predefined_transaction.py b/core/src/apps/solana/predefined_transaction.py
index ee44cf13..aca789f9 100644
--- a/core/src/apps/solana/predefined_transaction.py
+++ b/core/src/apps/solana/predefined_transaction.py
@@ -12,6 +12,7 @@ from .transaction.instructions import (
Token2022ProgramTransferCheckedInstruction,
TokenProgramTransferCheckedInstruction,
)
+from .types import is_address_reference
if TYPE_CHECKING:
from typing import Type
@@ -306,6 +307,11 @@ async def try_confirm_staking_transaction(
if stake_account != delegate.initialized_stake_account[0]:
return False
+ if is_address_reference(delegate.vote_account) or is_address_reference(
+ delegate.initialized_stake_account
+ ):
+ return False
+
await confirm_stake_transaction(
fee=fee,
signer_path=signer_path,
@@ -334,6 +340,8 @@ async def try_confirm_staking_transaction(
for withdraw in instructions:
if signer_public_key != withdraw.withdrawal_authority[0]:
return False
+ if is_address_reference(withdraw.recipient_account):
+ return False
if signer_public_key != withdraw.recipient_account[0]:
await confirm_claim_recipient(withdraw.recipient_account[0])
total_amount += withdraw.lamports
diff --git a/core/src/apps/solana/sign_tx.py b/core/src/apps/solana/sign_tx.py
index 8101f7c0..469d0bb4 100644
--- a/core/src/apps/solana/sign_tx.py
+++ b/core/src/apps/solana/sign_tx.py
@@ -7,7 +7,7 @@ from apps.common.keychain import with_slip44_keychain
from . import CURVE, PATTERNS, SLIP44_ID
from .transaction import Transaction
-from .types import AdditionalTxInfo
+from .types import AdditionalTxInfo, is_address_reference
if TYPE_CHECKING:
from trezor.messages import SolanaSignTx, SolanaTxSignature
@@ -82,6 +82,10 @@ async def sign_tx(
transfer_instructions = get_native_transfer_instructions(visible_instructions)
if transfer_instructions:
for transfer_instruction in transfer_instructions:
+ if is_address_reference(transfer_instruction.recipient_account):
+ raise DataError(
+ "Payment request cannot be used with ALT-referenced accounts"
+ )
verifier.add_output(
transfer_instruction.lamports,
base58.encode(transfer_instruction.recipient_account[0]),
@@ -91,6 +95,10 @@ async def sign_tx(
visible_instructions
)
for transfer_token_instruction in token_transfer_instructions:
+ if is_address_reference(transfer_token_instruction.destination_account):
+ raise DataError(
+ "Payment request cannot be used with ALT-referenced accounts"
+ )
verifier.add_output(
transfer_token_instruction.amount,
base58.encode(transfer_token_instruction.destination_account[0]),
diff --git a/core/src/apps/solana/transaction/__init__.py b/core/src/apps/solana/transaction/__init__.py
index 42e454e5..b666bb79 100644
--- a/core/src/apps/solana/transaction/__init__.py
+++ b/core/src/apps/solana/transaction/__init__.py
@@ -9,7 +9,7 @@ from ..constants import (
SOLANA_BASE_FEE_LAMPORTS,
SOLANA_COMPUTE_UNIT_LIMIT,
)
-from ..types import AddressType
+from ..types import AddressType, is_address_reference
from .instruction import Instruction
from .instructions import (
COMPUTE_BUDGET_PROGRAM_ID,
@@ -225,7 +225,8 @@ class Transaction:
def calculate_fee(self) -> Fee | None:
number_of_signers = 0
for address in self.addresses:
- if address[1] == AddressType.AddressSig:
+ # Writable or ReadOnly signers.
+ if address[1] in (AddressType.AddressSig, AddressType.AddressSigReadOnly):
number_of_signers += 1
base_fee = SOLANA_BASE_FEE_LAMPORTS * number_of_signers
@@ -264,7 +265,7 @@ class Transaction:
)
def get_account_address(self, account: Account) -> bytes | None:
- if len(account) == 2:
+ if not is_address_reference(account):
return account[0]
else:
# AddressReference points to an Address Lookup Table account, whose contents are unavailable here:
diff --git a/core/src/apps/solana/types.py b/core/src/apps/solana/types.py
index e5541eff..61b9722b 100644
--- a/core/src/apps/solana/types.py
+++ b/core/src/apps/solana/types.py
@@ -9,7 +9,7 @@ if TYPE_CHECKING:
from trezor.messages import SolanaTxAdditionalInfo, SolanaTxTokenAccountInfo
from trezor.utils import BufferReader
- from typing_extensions import Self
+ from typing_extensions import Self, TypeIs
Address = tuple[bytes, "AddressType"]
AddressReference = tuple[bytes, int, "AddressType"]
@@ -37,6 +37,18 @@ class AddressType(IntEnum):
AddressRw = 3
+def is_address_reference(account: Account) -> TypeIs[AddressReference]:
+ account_len = len(account)
+ if account_len == 2:
+ # Account included in the transaction directly.
+ return False
+ elif account_len == 3:
+ # Lookup table address reference.
+ return True
+ else:
+ raise ValueError(f"Invalid account length: {account_len}")
+
+
class PropertyTemplate(Generic[T]):
def __init__(
self,
Why this scored 58/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.