Skip to content

Commit 819812e

Browse files
committed
feat(ethereum): Add erc-4626 claim flow.
- Changelog previously added. [no changelog]
1 parent 39c051b commit 819812e

10 files changed

Lines changed: 519 additions & 7 deletions

File tree

common/tests/fixtures/ethereum/sign_tx.json

Lines changed: 105 additions & 0 deletions
Large diffs are not rendered by default.

core/src/apps/ethereum/layout.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
confirm_blob,
77
confirm_ethereum_staking_tx,
88
confirm_ethereum_vault_tx,
9+
confirm_ethereum_vault_claim,
910
confirm_text,
1011
should_show_more,
1112
)
@@ -290,6 +291,25 @@ async def require_confirm_vault_tx(
290291
br_name=br_name,
291292
)
292293

294+
async def require_confirm_claim_rewards(
295+
address_n: list[int],
296+
maximum_fee: str,
297+
fee_info_items: Iterable[StrPropertyType],
298+
token_labels: list[str],
299+
) -> None:
300+
account, account_path = get_account_and_path(address_n)
301+
token_list = "\n".join(token_labels)
302+
await confirm_ethereum_vault_claim(
303+
title=TR.ethereum__rewards_claim,
304+
intro_question=TR.ethereum__vault_claim_intro,
305+
account=account,
306+
account_path=account_path,
307+
maximum_fee=maximum_fee,
308+
info_items=fee_info_items,
309+
token_list=token_list,
310+
br_name="ethereum/vault/claim",
311+
)
312+
293313

294314
async def require_confirm_unstake(
295315
addr_bytes: bytes,

core/src/apps/ethereum/sc_constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from ubinascii import unhexlify
22

3+
from .yielding_vaults import KNOWN_VAULT
4+
35
KNOWN_ADDRESSES = {
46
# https://github.com/LedgerHQ/clear-signing-erc7730-registry/blob/master/registry/1inch/calldata-AggregationRouterV6.json#L9
57
unhexlify(
@@ -11,4 +13,5 @@
1113
unhexlify("68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"): "Uniswap V3 Router",
1214
# https://etherscan.io/address/0xe592427a0aece92de3edee1f18e0157c05861564
1315
unhexlify("e592427a0aece92de3edee1f18e0157c05861564"): "Uniswap V3 Router",
16+
KNOWN_VAULT[0]: KNOWN_VAULT[2],
1417
}

core/src/apps/ethereum/sign_tx.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ async def confirm_tx_data(
237237
return staking_approver
238238

239239
yielding_approver = yielding.get_approver(
240-
msg, network, address_bytes, maximum_fee, fee_items, sender_bytes
240+
msg, initial_data, network, address_bytes, maximum_fee, fee_items, sender_bytes
241241
)
242242
if yielding_approver is not None:
243243
if payment_request_verifier is not None:

core/src/apps/ethereum/yielding.py

Lines changed: 145 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from trezor.utils import BufferReader
44
from trezor.wire import DataError
5+
from trezor import log
56

67
from .yielding_vaults import lookup_vault
78

@@ -19,6 +20,7 @@
1920
FUNC_SIG_DEPOSIT = b"\x6e\x55\x3f\x65"
2021
FUNC_SIG_WITHDRAW = b"\xb4\x60\xaf\x94"
2122
FUNC_SIG_REDEEM = b"\xba\x08\x76\x52"
23+
FUNC_SIG_CLAIM = b"\x71\xee\x95\xc0"
2224

2325
if __debug__:
2426
from trezor.crypto.hashlib import sha3_256
@@ -35,33 +37,53 @@
3537
FUNC_SIG_REDEEM
3638
== sha3_256(b"redeem(uint256,address,address)", keccak=True).digest()[:4]
3739
)
38-
40+
assert (
41+
FUNC_SIG_CLAIM
42+
== sha3_256(b"claim(address[],address[],uint256[],bytes32[][])", keccak=True).digest()[:4]
43+
)
3944

4045
def get_approver(
4146
msg: MsgInSignTx,
47+
initial_data: AnyBytes,
4248
network: EthereumNetworkInfo,
4349
address_bytes: AnyBytes,
4450
maximum_fee: str,
4551
fee_items: Iterable[StrPropertyType],
4652
sender_bytes: AnyBytes,
4753
) -> tuple[ConfirmDataFn, Coroutine[Any, Any, None]] | None:
4854

55+
log.debug("SS DEBUG", "inside yielding get_approver")
56+
log.debug("SS DEBUG", "data_length=%d initial_data_len=%d", msg.data_length, len(initial_data))
57+
log.debug("SS DEBUG", "to=%s chain_id=%d", msg.to, network.chain_id)
58+
log.debug("SS DEBUG", "sender_bytes=%s", sender_bytes)
59+
4960
from .clear_signing import SC_FUNC_SIG_BYTES
5061
from .helpers import get_progress_indicator
5162

52-
if msg.data_length > len(msg.data_initial_chunk):
63+
if msg.data_length > len(initial_data):
64+
log.debug("SS DEBUG", "get_approver: data exceeds buffer, returning None (data_length=%d > initial_data=%d)", msg.data_length, len(initial_data))
5365
return None
5466

55-
data_reader = BufferReader(msg.data_initial_chunk)
67+
data_reader = BufferReader(initial_data)
5668
if data_reader.remaining_count() < SC_FUNC_SIG_BYTES:
69+
log.debug("SS DEBUG", "get_approver: not enough data for func sig, remaining=%d", data_reader.remaining_count())
5770
return None
5871

5972
is_known_vault, vault_str, asset_token, vault_token = lookup_vault(
6073
network, address_bytes
6174
)
75+
log.debug("SS DEBUG", "lookup_vault: is_known=%s vault_str=%s", is_known_vault, vault_str)
76+
log.debug("SS DEBUG", "lookup_vault: asset_token=%s vault_token=%s", asset_token.symbol if asset_token else None, vault_token.symbol if vault_token else None)
6277

6378
handler = None
6479
func_sig = data_reader.read_memoryview(SC_FUNC_SIG_BYTES)
80+
log.debug("SS DEBUG", "func_sig=%s", func_sig)
81+
log.debug("SS DEBUG", "func_sig matches DEPOSIT=%s WITHDRAW=%s REDEEM=%s CLAIM=%s",
82+
func_sig == FUNC_SIG_DEPOSIT,
83+
func_sig == FUNC_SIG_WITHDRAW,
84+
func_sig == FUNC_SIG_REDEEM,
85+
func_sig == FUNC_SIG_CLAIM,
86+
)
6587
if func_sig in (FUNC_SIG_DEPOSIT, FUNC_SIG_WITHDRAW, FUNC_SIG_REDEEM):
6688
token = vault_token if func_sig == FUNC_SIG_REDEEM else asset_token
6789
handler = _prepare_vault_tx(
@@ -76,10 +98,25 @@ def get_approver(
7698
token=token,
7799
func_sig=func_sig,
78100
)
101+
elif func_sig == FUNC_SIG_CLAIM:
102+
log.debug("SS DEBUG", "get_approver: routing to _prepare_claim_rewards")
103+
handler = _prepare_claim_rewards(
104+
data_reader=data_reader,
105+
msg=msg,
106+
network=network,
107+
maximum_fee=maximum_fee,
108+
fee_items=fee_items,
109+
sender_bytes=sender_bytes,
110+
is_known_vault=is_known_vault,
111+
)
112+
else:
113+
log.debug("SS DEBUG", "get_approver: func_sig not recognized, handler=None -> blind signing")
79114

115+
log.debug("SS DEBUG", "get_approver: handler=%s", handler)
80116
if handler is not None:
81117
progress_indicator = get_progress_indicator(msg.data_length)
82118
return progress_indicator, handler
119+
log.debug("SS DEBUG", "get_approver: returning None -> will fall through to blind/clear signing")
83120
return None
84121

85122

@@ -119,7 +156,7 @@ def _prepare_vault_tx(
119156
):
120157
raise ValueError
121158
except (ValueError, EOFError, InvalidFunctionCall):
122-
raise DataError("Invalid data for ERC-4626 vault transaction")
159+
raise DataError(f"Invalid data for ERC-4626 vault transaction. Remaining data count: {data_reader.remaining_count()}, amount: {amount}, eth_Value: {int.from_bytes(msg.value, 'big')}")
123160

124161
if not _is_vault_tx_safe(is_known_vault, sender_bytes, receiver_bytes, owner_bytes):
125162
return None
@@ -135,22 +172,126 @@ def _prepare_vault_tx(
135172
)
136173

137174

175+
def _prepare_claim_rewards(
176+
data_reader: BufferReader,
177+
msg: MsgInSignTx,
178+
network: EthereumNetworkInfo,
179+
maximum_fee: str,
180+
fee_items: Iterable[StrPropertyType],
181+
sender_bytes: AnyBytes,
182+
is_known_vault: bool,
183+
) -> "Coroutine[Any, Any, None] | None":
184+
from .clear_signing import InvalidFunctionCall, parse_address
185+
from .layout import require_confirm_claim_rewards
186+
from .yielding_vaults import get_token_label
187+
188+
log.debug("SS DEBUG", "_prepare_claim_rewards: entered, is_known_vault=%s", is_known_vault)
189+
log.debug("SS DEBUG", "_prepare_claim_rewards: sender_bytes=%s", sender_bytes)
190+
191+
# claim(address[] users, address[] tokens, uint256[] amounts, bytes32[][] proofs)
192+
# All 4 params are dynamic; first 128 bytes are their ABI offsets.
193+
try:
194+
data = data_reader.read_memoryview(data_reader.remaining_count())
195+
log.debug("SS DEBUG", "_prepare_claim_rewards: data len=%d", len(data))
196+
197+
if len(data) < 128:
198+
log.debug("SS DEBUG", "_prepare_claim_rewards: data too short (<128), raising InvalidFunctionCall")
199+
raise InvalidFunctionCall
200+
201+
users_param_offset = int.from_bytes(data[0:32], "big")
202+
tokens_param_offset = int.from_bytes(data[32:64], "big")
203+
amounts_param_offset = int.from_bytes(data[64:96], "big")
204+
proofs_param_offset = int.from_bytes(data[96:128], "big")
205+
log.debug("SS DEBUG", "offsets: users=%d tokens=%d amounts=%d proofs=%d", users_param_offset, tokens_param_offset, amounts_param_offset, proofs_param_offset)
206+
207+
if proofs_param_offset + 32 > len(data):
208+
log.debug("SS DEBUG", "_prepare_claim_rewards: proofs_param_offset=%d + 32 > data len=%d, raising InvalidFunctionCall", proofs_param_offset, len(data))
209+
raise InvalidFunctionCall
210+
211+
users_array_length = int.from_bytes(data[users_param_offset : users_param_offset + 32], "big")
212+
tokens_array_length = int.from_bytes(data[tokens_param_offset : tokens_param_offset + 32], "big")
213+
amounts_array_length = int.from_bytes(data[amounts_param_offset : amounts_param_offset + 32], "big")
214+
proofs_array_length = int.from_bytes(data[proofs_param_offset : proofs_param_offset + 32], "big")
215+
log.debug("SS DEBUG", "array lengths: users=%d tokens=%d amounts=%d proofs=%d", users_array_length, tokens_array_length, amounts_array_length, proofs_array_length)
216+
217+
if (
218+
users_array_length != tokens_array_length
219+
or tokens_array_length != amounts_array_length
220+
or amounts_array_length != proofs_array_length
221+
):
222+
log.debug("SS DEBUG", "_prepare_claim_rewards: array length mismatch! users=%d tokens=%d amounts=%d proofs=%d", users_array_length, tokens_array_length, amounts_array_length, proofs_array_length)
223+
raise ValueError
224+
225+
user_address = parse_address(data[users_param_offset + 32 : users_param_offset + 64])
226+
log.debug("SS DEBUG", "user_address[0]=%s type=%s", user_address, type(user_address))
227+
if not isinstance(user_address, bytes):
228+
log.debug("SS DEBUG", "_prepare_claim_rewards: user_address is not bytes, raising InvalidFunctionCall")
229+
raise InvalidFunctionCall
230+
231+
# Check if all users are the same. We validate if it's the sender in _is_vault_tx_safe()
232+
# If either of these conditions are unmet, we revert to blind signing (return None).
233+
for i in range(1, users_array_length):
234+
addr_start = users_param_offset + 32 + i * 32
235+
other = parse_address(data[addr_start : addr_start + 32])
236+
log.debug("SS DEBUG", "users[%d]=%s == users[0]=%s -> %s", i, other, user_address, other == user_address)
237+
if other != user_address:
238+
log.debug("SS DEBUG", "_prepare_claim_rewards: user[%d] != user[0], returning None (blind sign)", i)
239+
return None
240+
241+
token_labels: list[str] = []
242+
for i in range(tokens_array_length):
243+
addr_start = tokens_param_offset + 32 + i * 32
244+
addr = parse_address(data[addr_start : addr_start + 32])
245+
if not isinstance(addr, bytes):
246+
log.debug("SS DEBUG", "_prepare_claim_rewards: token[%d] addr not bytes, raising InvalidFunctionCall", i)
247+
raise InvalidFunctionCall
248+
label = get_token_label(addr, network)
249+
log.debug("SS DEBUG", "token[%d] addr=%s label=%s", i, addr, label)
250+
token_labels.append(label)
251+
252+
except (ValueError, EOFError, InvalidFunctionCall):
253+
raise DataError("Invalid data for claim rewards transaction")
254+
255+
log.debug("SS DEBUG", "_prepare_claim_rewards: token_labels=%s", token_labels)
256+
log.debug("SS DEBUG", "_prepare_claim_rewards: calling _is_vault_tx_safe sender=%s user_address=%s", sender_bytes, user_address)
257+
258+
if not _is_vault_tx_safe(is_known_vault, sender_bytes, user_address):
259+
log.debug("SS DEBUG", "_prepare_claim_rewards: _is_vault_tx_safe returned False, returning None")
260+
return None
261+
262+
log.debug("SS DEBUG", "_prepare_claim_rewards: all checks passed, returning confirm coroutine")
263+
return require_confirm_claim_rewards(
264+
address_n=msg.address_n,
265+
maximum_fee=maximum_fee,
266+
fee_info_items=fee_items,
267+
token_labels=token_labels,
268+
)
269+
138270
def _is_vault_tx_safe(
139271
is_known_vault: bool,
140272
sender_bytes: AnyBytes,
141273
receiver_bytes: AnyBytes,
142274
owner_bytes: AnyBytes | None = None,
143275
) -> bool:
144276

277+
log.debug("SS DEBUG", "_is_vault_tx_safe: is_known_vault=%s", is_known_vault)
278+
log.debug("SS DEBUG", "_is_vault_tx_safe: sender_bytes=%s", sender_bytes)
279+
log.debug("SS DEBUG", "_is_vault_tx_safe: receiver_bytes=%s", receiver_bytes)
280+
log.debug("SS DEBUG", "_is_vault_tx_safe: sender==receiver -> %s", receiver_bytes == sender_bytes)
281+
145282
is_calldata_safe = receiver_bytes == sender_bytes
146283
if owner_bytes is not None:
147284
# Withdraw/redeem transaction
285+
log.debug("SS DEBUG", "_is_vault_tx_safe: owner_bytes=%s sender==owner -> %s", owner_bytes, owner_bytes == sender_bytes)
148286
is_calldata_safe = is_calldata_safe and owner_bytes == sender_bytes
149287

288+
log.debug("SS DEBUG", "_is_vault_tx_safe: is_calldata_safe=%s", is_calldata_safe)
150289
if is_calldata_safe:
151290
return True
152291
else:
153292
# Hard fail for known (Trezor) vaults, blind sign for unknown vaults
154293
if is_known_vault:
294+
log.debug("SS DEBUG", "_is_vault_tx_safe: known vault mismatch -> raising DataError")
155295
raise DataError("Vault tx: Signer receiver mismatch")
296+
log.debug("SS DEBUG", "_is_vault_tx_safe: unknown vault mismatch -> returning False (blind sign)")
156297
return False

core/src/apps/ethereum/yielding_vaults.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# Test vault: https://etherscan.io/address/0xa511d618cD0F9d7cAD791009d7c5E3b19c9568da
1313
b"\xa5\x11\xd6\x18\xcd\x0f\x9d\x7c\xad\x79\x10\x09\xd7\xc5\xe3\xb1\x9c\x95\x68\xda", # vault contract address
1414
1, # chain id (Ethereum)
15-
"Test Steakhouse USDC Prime Vault", # owner/protocol name
15+
"Test Steakhouse USDC Prime Vault", # Vault Name
1616
# Asset Token
1717
EthereumTokenInfo(
1818
symbol="USDC",
@@ -31,6 +31,16 @@
3131
),
3232
)
3333

34+
def get_token_label(token_addr: AnyBytes, network: EthereumNetworkInfo) -> str:
35+
# Only known token for now. We can use clear signing here to request token definitions from Connect, once it is done.
36+
# https://etherscan.io/token/0x58d97b57bb95320f9a05dc918aef65434969c2b2
37+
_MORPHO_ADDR = b"\x58\xd9\x7b\x57\xbb\x95\x32\x0f\x9a\x05\xdc\x91\x8a\xef\x65\x43\x49\x69\xc2\xb2"
38+
39+
if token_addr == _MORPHO_ADDR and network.chain_id == 1:
40+
return "MORPHO"
41+
else:
42+
return "UNKNOWN"
43+
3444

3545
def lookup_vault(
3646
network: EthereumNetworkInfo, vault_addr: AnyBytes

core/src/trezor/ui/layouts/bolt/__init__.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1370,7 +1370,7 @@ async def confirm_ethereum_vault_tx(
13701370
await confirm_value(
13711371
title=title,
13721372
value=vault_str,
1373-
description="",
1373+
description=verb,
13741374
br_name=br_name + "/vault",
13751375
br_code=br_code,
13761376
verb=TR.buttons__continue,
@@ -1398,6 +1398,55 @@ async def confirm_ethereum_vault_tx(
13981398
br_code=br_code,
13991399
)
14001400

1401+
async def confirm_ethereum_vault_claim(
1402+
title: str,
1403+
intro_question: str,
1404+
account: str | None,
1405+
account_path: str | None,
1406+
maximum_fee: str,
1407+
info_items: Iterable[StrPropertyType],
1408+
token_list: str,
1409+
br_name: str,
1410+
) -> None:
1411+
1412+
account_properties: list[StrPropertyType] = []
1413+
if account:
1414+
account_properties.append((TR.words__account, account, None))
1415+
if account_path:
1416+
account_properties.append(
1417+
(TR.address_details__derivation_path, account_path, None)
1418+
)
1419+
1420+
await confirm_value(
1421+
title=title,
1422+
value=intro_question,
1423+
description="",
1424+
br_name=br_name + "/intro",
1425+
verb=TR.buttons__continue,
1426+
is_data=False,
1427+
info_items=account_properties if account_properties else None,
1428+
info_title=TR.address_details__account_info,
1429+
)
1430+
1431+
await confirm_properties(
1432+
br_name=br_name + "/tokens",
1433+
title=title,
1434+
props=[
1435+
(TR.ethereum__reward_tokens, token_list, False)
1436+
],
1437+
)
1438+
1439+
await _confirm_summary(
1440+
amount=None,
1441+
amount_label=None,
1442+
fee=maximum_fee,
1443+
fee_label=TR.send__maximum_fee,
1444+
title=title,
1445+
extra_items=info_items,
1446+
extra_title=TR.confirm_total__title_fee,
1447+
br_name=br_name + "/summary",
1448+
)
1449+
14011450
def confirm_solana_unknown_token_warning() -> Awaitable[None]:
14021451
return show_danger(
14031452
"unknown_token_warning", content=TR.solana__unknown_token_address

0 commit comments

Comments
 (0)