22
33from trezor .wire import DataError
44
5- from .clear_signing import Atomic , DisplayFormat , parse_address , parse_uint256
5+ from .clear_signing import Array , Atomic , DisplayFormat , parse_address , parse_uint256
66from .yielding_vaults import UNKNOWN_VAULT , lookup_vault
77
88if TYPE_CHECKING :
8282 field_definitions = [],
8383)
8484
85+ # claim(address[] users, address[] tokens, uint256[] amounts, bytes32[][] proofs)
86+ # The proofs parameter is intentionally omitted from `parameter_definitions`:
87+ # we don't display it, so we skip the per-element parsing/allocations and only
88+ # manually validate the top-level array structure (see `_prepare_merkl_claim`).
89+ CLAIM_DISPLAY_FORMAT = DisplayFormat (
90+ binding_context = None ,
91+ func_sig = FUNC_SIG_CLAIM ,
92+ intent = "Claim" ,
93+ parameter_definitions = [
94+ Array (Atomic (parse_address )), # users
95+ Array (Atomic (parse_address )), # tokens
96+ Array (Atomic (parse_uint256 )), # amounts
97+ ],
98+ field_definitions = [],
99+ )
100+
85101
86102async def get_approver (
87103 msg : MsgInSignTx ,
@@ -131,7 +147,7 @@ async def get_approver(
131147 token = token ,
132148 )
133149 elif func_sig == FUNC_SIG_CLAIM :
134- handler = _prepare_claim_rewards (
150+ handler = await _prepare_merkl_claim (
135151 calldata = calldata ,
136152 msg = msg ,
137153 network = network ,
@@ -200,7 +216,7 @@ async def _prepare_vault_tx(
200216 )
201217
202218
203- def _prepare_claim_rewards (
219+ async def _prepare_merkl_claim (
204220 calldata : memoryview ,
205221 msg : MsgInSignTx ,
206222 network : EthereumNetworkInfo ,
@@ -209,81 +225,82 @@ def _prepare_claim_rewards(
209225 sender_bytes : AnyBytes ,
210226) -> Coroutine [Any , Any , None ] | None :
211227
212- return None
213- # TODO: Finalize the UI in the next iteration.
214-
215- from .clear_signing import InvalidFunctionCall , parse_address
228+ from .clear_signing import InvalidFunctionCall
229+ from .definitions import Definitions
216230 from .layout import require_confirm_claim_rewards
217231 from .yielding_vaults import get_token_label
218232
219233 _MERKL_XYZ_CLAIM_DISTRIBUTOR_ADDR = "0x3ef3d8ba38ebe18db133cec108f4d14ce00dd9ae"
220234
221235 if int .from_bytes (msg .value , "big" ) != 0 :
222236 raise DataError (
223- "Non-zero ETH transfer with ERC-4626 vault transaction not allowed"
237+ "Non-zero ETH transfer with claim rewards transaction not allowed"
224238 )
225239
226- # claim(address[] users, address[] tokens, uint256[] amounts, bytes32[][] proofs)
227- # All 4 params are dynamic; first 128 bytes are their ABI offsets (relative to abi_base).
228- try :
229- from trezor .utils import BufferReader
230-
231- data_reader = BufferReader (bytes (calldata ))
232- param_base = data_reader .offset
233-
234- receivers_param_offset = int .from_bytes (data_reader .read_memoryview (32 ), "big" )
235- tokens_param_offset = int .from_bytes (data_reader .read_memoryview (32 ), "big" )
236- amounts_param_offset = int .from_bytes (data_reader .read_memoryview (32 ), "big" )
237- proofs_param_offset = int .from_bytes (data_reader .read_memoryview (32 ), "big" )
240+ defs = Definitions (network , {})
238241
239- def _read_array_length (param_offset : int ) -> int :
240- data_reader .seek (param_base + param_offset )
241- return int .from_bytes (data_reader .read_memoryview (32 ), "big" )
242+ try :
243+ parameters , _ = await CLAIM_DISPLAY_FORMAT .parse_calldata (calldata , msg , defs )
244+ users , tokens , amounts = parameters
245+ if (
246+ not isinstance (users , list )
247+ or not isinstance (tokens , list )
248+ or not isinstance (amounts , list )
249+ ):
250+ raise ValueError
242251
243- receivers_array_length = _read_array_length (receivers_param_offset )
244- tokens_array_length = _read_array_length (tokens_param_offset )
245- amounts_array_length = _read_array_length (amounts_param_offset )
246- proofs_array_length = _read_array_length (proofs_param_offset )
252+ # The proofs head sits at offset 96, right after the three parsed array
253+ # heads. We only validate that proofs is a well-formed top-level array
254+ # whose length matches the others — we never read its elements.
255+ _PROOFS_HEAD_OFFSET = 3 * 32
256+ if _PROOFS_HEAD_OFFSET + 32 > len (calldata ):
257+ raise ValueError
258+ proofs_body = int .from_bytes (
259+ calldata [_PROOFS_HEAD_OFFSET : _PROOFS_HEAD_OFFSET + 32 ], "big"
260+ )
261+ if proofs_body + 32 > len (calldata ):
262+ raise ValueError
263+ proofs_length = int .from_bytes (calldata [proofs_body : proofs_body + 32 ], "big" )
247264
248265 if (
249- receivers_array_length != tokens_array_length
250- or tokens_array_length != amounts_array_length
251- or amounts_array_length != proofs_array_length
266+ len ( users ) != len ( tokens )
267+ or len ( tokens ) != len ( amounts )
268+ or len ( amounts ) != proofs_length
252269 ):
253270 raise ValueError
254271
255- data_reader .seek (param_base + receivers_param_offset + 32 )
256- first_receiver_address = parse_address (data_reader .read_memoryview (32 ))
257- if not isinstance (first_receiver_address , bytes ):
258- raise InvalidFunctionCall
259-
260- # Check if all users are the same. We validate if it's the sender in _is_vault_tx_safe()
261- # If either of these conditions are unmet, we revert to blind signing (return None).
262- for i in range (1 , receivers_array_length ):
263- data_reader .seek (param_base + receivers_param_offset + 32 + i * 32 )
264- other = parse_address (data_reader .read_memoryview (32 ))
265- if other != first_receiver_address :
266- return None
267-
268- token_labels : list [str ] = []
269- for i in range (tokens_array_length ):
270- data_reader .seek (param_base + tokens_param_offset + 32 + i * 32 )
271- addr = parse_address (data_reader .read_memoryview (32 ))
272- if not isinstance (addr , bytes ):
273- raise InvalidFunctionCall
274- label = get_token_label (addr , network )
275- token_labels .append (label )
276-
277- except (ValueError , EOFError , InvalidFunctionCall ):
272+ if len (users ) == 0 :
273+ raise ValueError
274+
275+ first_user = users [0 ]
276+ if not isinstance (first_user , bytes ):
277+ raise ValueError
278+
279+ except (ValueError , InvalidFunctionCall ):
278280 raise DataError ("Invalid data for claim rewards transaction" )
279281
280- # We don't show claim flows for any unknown distributor for now.
282+ # All receivers must be the same; otherwise revert to blind signing.
283+ for other in users [1 :]:
284+ if other != first_user :
285+ return None
286+
287+ # We don't show claim flows for non claim.xyz or for non-signer users.
281288 if (
282- msg .to != _MERKL_XYZ_CLAIM_DISTRIBUTOR_ADDR
283- or sender_bytes != first_receiver_address
289+ msg .to . lower () != _MERKL_XYZ_CLAIM_DISTRIBUTOR_ADDR
290+ or sender_bytes != first_user
284291 ):
285292 return None
286293
294+ # Not sure about the UX if we fetch too many defintions so capping definition fetching to 4 for now.
295+ try_fetch_definitions = len (tokens ) <= 4
296+
297+ token_labels : list [str ] = []
298+ for token in tokens :
299+ if not isinstance (token , bytes ):
300+ raise DataError ("Invalid data for claim rewards transaction" )
301+ label = await get_token_label (token , network , msg , try_fetch_definitions )
302+ token_labels .append (label )
303+
287304 return require_confirm_claim_rewards (
288305 address_n = msg .address_n ,
289306 maximum_fee = maximum_fee ,
0 commit comments