Skip to content

Commit 515b040

Browse files
committed
feat(ethereum): Add Merkl claim support.
[no changelog]
1 parent ae653a8 commit 515b040

4 files changed

Lines changed: 303 additions & 61 deletions

File tree

common/tests/fixtures/ethereum/sign_tx.json

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,69 @@
509509
"sig_r": "de16620898c6fe3cc5b7d3c5ff6581c2a7845a69bd22f37559f610d6253cfd2f",
510510
"sig_s": "58e928b596c930f2f302386025e1eb2a74b342dd9005ae84a161df16a5a295bf"
511511
}
512+
},
513+
{
514+
"name": "merkl_claim_single_unknown_token",
515+
"skip_models": ["t1"],
516+
"parameters": {
517+
"comment": "Merkl claim: users=[signer], tokens=[0x0fedba...], amounts=[0x085985deab876834], proofs=[19 leaves]; known distributor on chain 1",
518+
"data": "71ee95c0000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000010000000000000000000000004f4f1488acb1ae1b46146ceff804f591dfe660ac00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000fedba9178b70e8b54e2af08ebffcf28a1e5a43b0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000085985deab87683400000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001364faf7f5ee9f5b459e66762232f6ab0279fd8ab6528df06e27f904c5c7e2423583b8ec99f8d6a98d35847722f25c4453f5197360fa17d47ec7e0fa5b1b13367a8a669530d902c20fb9fb9c3c69a3c572285fa43a040d7eb7d22f362774686d7c5ff909d98eda2b5db084e42339c2a429e34d427339064e30dd763d1a562d576479f6a3d276b2f67390420fa6b438c9347f2b6049af245a238f2c00899d581884b1e04381651c085a309bfc472c014ac6781c44563b499a33f6aad8e4f04cb2f50c79f2fef01dd4d5228bc0863dd463e1c42464a477cabcbe358760aaef395b67cff9336d50615279a714d0fda73682cd76ecb79a793cc67444d9c9bf84617094a8888ec415452794c573fd4763f4d007b6ed0b9144b5e44ef415a62d559594e90d3b14e9727ccd373a6ff9de79d81004f8d9438d15b170fc4e17a95792037b2f62394b48c45e42c413842e6527d2cb580abb0027d68f4737fe22aa1c78fe48b39760b3d79bf093b92f1b1e06b3466262eecf1fcec634b9f7a297c5bb1d826c8052366d43298286026e2adc9097ad1ce23343a749afa3eda75e9a46a36078fad8ec53b10425ef4c741b3080f0e202cb991edb8964064761e0b220f0bc8ddc10a2a7b6d0f3bfaf8fd330edb1214a8ae12dfe77932a5bee0e973007fa6f02912ee554616811ddc8a74543f7a51956d3435c5503ce74b10a23807d5f055df5ae3b33a4501aa45cbd1e051bb049fd59be3681e03684341ff3ef684d2ee67037d6190504e460c7f56243d1808b4653f0c257dba19729d5b181ef43ba0fe1e802c950a6a6fd6fa408e79277be3e34537fd5f5e060bd9963ac61d06f81be7bf28af8bbaa",
519+
"path": "m/44'/60'/0'/0/1",
520+
"to_address": "0x3Ef3D8bA38EBe18DB133cEc108f4D14CE00Dd9Ae",
521+
"chain_id": 1,
522+
"nonce": "0x0",
523+
"gas_price": "0x14",
524+
"gas_limit": "0x14",
525+
"tx_type": null,
526+
"value": "0x0"
527+
},
528+
"result": {
529+
"sig_v": 38,
530+
"sig_r": "bf92bf4da6889e835172b43c4432c7be5cd4c951c12100070ae8f9c6efa89b6c",
531+
"sig_s": "2319eb5be80352885dade9f0c8ff9f437493cdf8dd1490d0cb36ec312a34b4da"
532+
}
533+
},
534+
{
535+
"name": "merkl_claim_multiple_tokens",
536+
"skip_models": ["t1"],
537+
"parameters": {
538+
"comment": "Merkl claim: users=[signer, signer], tokens=[0x58d97b..., 0x0fedba...], amounts=[0x16ffd023e945f7, 0x085985deab876834], proofs=[19 leaves each]; known distributor on chain 1",
539+
"data": "71ee95c0000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000000020000000000000000000000004f4f1488acb1ae1b46146ceff804f591dfe660ac0000000000000000000000004f4f1488acb1ae1b46146ceff804f591dfe660ac000000000000000000000000000000000000000000000000000000000000000200000000000000000000000058d97b57bb95320f9a05dc918aef65434969c2b20000000000000000000000000fedba9178b70e8b54e2af08ebffcf28a1e5a43b00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000016ffd023e945f7000000000000000000000000000000000000000000000000085985deab8768340000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000002c0000000000000000000000000000000000000000000000000000000000000013569aebf90ff10ac097a50546701ae153e99286f8c241f4ed91106ead272cce863810cfb729513e166b992002039f613de7bdcd6c45ce0a888499b870b1eab46e3d4a652b2a0ae46ec2a113face89d447c92284a298dccbe47d684bd53b64232d876b2fc7136fa1043ebb3b844dcdd9c13f4422cafe5173fe62a7d2bf709f5197ce3a5573d31c95d286d19db7a421e12c57c1eef6786cee048b50a5e033e5b2e31e24183247a8015a7be1dcc25ad0842d764f2634e10387e87774fd29e82f0e5ade85c3ed85fde553a7bb2d735b0ec35f45d8c385409b2dc44a5ec69857ddf445c8ffe9f1841ae94a9e9347648d98a03104c6069f3275264837a9717e9bef6777e518f9843d605f5294b8396c4a3c605f6e8cbba45fbeed07ef538004b51ee109f3f5d8a356cfea44c0b179dcf9d383c3eee20b65ebd1cddd5bba69837e4857362c1abdd40db583a507462a94fc8c0082281c10b22cacf3ed22141d597da373f3e4590ad59736eab415ed0e0aa47cbaffb3317efe1ee85cbf2e470882c6622a791fbc20939ec1866c6c1e6e6fc2a06122af055eba428da8edd234189b2ef4af57333313fb84b79c8a37e19dff644d59d67249dd91c70f0241429b25d27332bacbfb50f6cd33f88ee0e657b03e33878d301aecfec3e7203919cc6b0a666ffd39b43742e6aa9b332c9bfd5d5283373c5449dd4ce43da5bef2c7ca459b8177a16953a4501aa45cbd1e051bb049fd59be3681e03684341ff3ef684d2ee67037d6190504e460c7f56243d1808b4653f0c257dba19729d5b181ef43ba0fe1e802c950a6a6fd6fa408e79277be3e34537fd5f5e060bd9963ac61d06f81be7bf28af8bbaa000000000000000000000000000000000000000000000000000000000000001364faf7f5ee9f5b459e66762232f6ab0279fd8ab6528df06e27f904c5c7e2423583b8ec99f8d6a98d35847722f25c4453f5197360fa17d47ec7e0fa5b1b13367a8a669530d902c20fb9fb9c3c69a3c572285fa43a040d7eb7d22f362774686d7c5ff909d98eda2b5db084e42339c2a429e34d427339064e30dd763d1a562d576479f6a3d276b2f67390420fa6b438c9347f2b6049af245a238f2c00899d581884b1e04381651c085a309bfc472c014ac6781c44563b499a33f6aad8e4f04cb2f50c79f2fef01dd4d5228bc0863dd463e1c42464a477cabcbe358760aaef395b67cff9336d50615279a714d0fda73682cd76ecb79a793cc67444d9c9bf84617094a8888ec415452794c573fd4763f4d007b6ed0b9144b5e44ef415a62d559594e90d3b14e9727ccd373a6ff9de79d81004f8d9438d15b170fc4e17a95792037b2f62394b48c45e42c413842e6527d2cb580abb0027d68f4737fe22aa1c78fe48b39760b3d79bf093b92f1b1e06b3466262eecf1fcec634b9f7a297c5bb1d826c8052366d43298286026e2adc9097ad1ce23343a749afa3eda75e9a46a36078fad8ec53b10425ef4c741b3080f0e202cb991edb8964064761e0b220f0bc8ddc10a2a7b6d0f3bfaf8fd330edb1214a8ae12dfe77932a5bee0e973007fa6f02912ee554616811ddc8a74543f7a51956d3435c5503ce74b10a23807d5f055df5ae3b33a4501aa45cbd1e051bb049fd59be3681e03684341ff3ef684d2ee67037d6190504e460c7f56243d1808b4653f0c257dba19729d5b181ef43ba0fe1e802c950a6a6fd6fa408e79277be3e34537fd5f5e060bd9963ac61d06f81be7bf28af8bbaa",
540+
"path": "m/44'/60'/0'/0/1",
541+
"to_address": "0x3Ef3D8bA38EBe18DB133cEc108f4D14CE00Dd9Ae",
542+
"chain_id": 1,
543+
"nonce": "0x0",
544+
"gas_price": "0x14",
545+
"gas_limit": "0x14",
546+
"tx_type": null,
547+
"value": "0x0"
548+
},
549+
"result": {
550+
"sig_v": 38,
551+
"sig_r": "eb321d658cdc5b2ede5d5e63bf663b2193f95f9c0ad1120aa809b9e8009367e2",
552+
"sig_s": "7c32bc4951f877e981652e7f90f4e7654c393f55238408327039c765192a7a00"
553+
}
554+
},
555+
{
556+
"name": "merkl_claim_multiple_different_users",
557+
"skip_models": ["t1"],
558+
"parameters": {
559+
"comment": "Merkl claim: users=[0x9ea3..., 0x9ea3...] (not the signer), tokens=[0x58d97b..., 0x0fedba...], amounts=[0x16ffd023e945f7, 0x085985deab876834]; unknown distributor on chain 8453",
560+
"data": "71ee95c0000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000000020000000000000000000000009ea3721b5bf3b64b4418c38b603154d2d597fae30000000000000000000000009ea3721b5bf3b64b4418c38b603154d2d597fae3000000000000000000000000000000000000000000000000000000000000000200000000000000000000000058d97b57bb95320f9a05dc918aef65434969c2b20000000000000000000000000fedba9178b70e8b54e2af08ebffcf28a1e5a43b00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000016ffd023e945f7000000000000000000000000000000000000000000000000085985deab8768340000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000002c0000000000000000000000000000000000000000000000000000000000000013569aebf90ff10ac097a50546701ae153e99286f8c241f4ed91106ead272cce863810cfb729513e166b992002039f613de7bdcd6c45ce0a888499b870b1eab46e3d4a652b2a0ae46ec2a113face89d447c92284a298dccbe47d684bd53b64232d876b2fc7136fa1043ebb3b844dcdd9c13f4422cafe5173fe62a7d2bf709f5197ce3a5573d31c95d286d19db7a421e12c57c1eef6786cee048b50a5e033e5b2e31e24183247a8015a7be1dcc25ad0842d764f2634e10387e87774fd29e82f0e5ade85c3ed85fde553a7bb2d735b0ec35f45d8c385409b2dc44a5ec69857ddf445c8ffe9f1841ae94a9e9347648d98a03104c6069f3275264837a9717e9bef6777e518f9843d605f5294b8396c4a3c605f6e8cbba45fbeed07ef538004b51ee109f3f5d8a356cfea44c0b179dcf9d383c3eee20b65ebd1cddd5bba69837e4857362c1abdd40db583a507462a94fc8c0082281c10b22cacf3ed22141d597da373f3e4590ad59736eab415ed0e0aa47cbaffb3317efe1ee85cbf2e470882c6622a791fbc20939ec1866c6c1e6e6fc2a06122af055eba428da8edd234189b2ef4af57333313fb84b79c8a37e19dff644d59d67249dd91c70f0241429b25d27332bacbfb50f6cd33f88ee0e657b03e33878d301aecfec3e7203919cc6b0a666ffd39b43742e6aa9b332c9bfd5d5283373c5449dd4ce43da5bef2c7ca459b8177a16953a4501aa45cbd1e051bb049fd59be3681e03684341ff3ef684d2ee67037d6190504e460c7f56243d1808b4653f0c257dba19729d5b181ef43ba0fe1e802c950a6a6fd6fa408e79277be3e34537fd5f5e060bd9963ac61d06f81be7bf28af8bbaa000000000000000000000000000000000000000000000000000000000000001364faf7f5ee9f5b459e66762232f6ab0279fd8ab6528df06e27f904c5c7e2423583b8ec99f8d6a98d35847722f25c4453f5197360fa17d47ec7e0fa5b1b13367a8a669530d902c20fb9fb9c3c69a3c572285fa43a040d7eb7d22f362774686d7c5ff909d98eda2b5db084e42339c2a429e34d427339064e30dd763d1a562d576479f6a3d276b2f67390420fa6b438c9347f2b6049af245a238f2c00899d581884b1e04381651c085a309bfc472c014ac6781c44563b499a33f6aad8e4f04cb2f50c79f2fef01dd4d5228bc0863dd463e1c42464a477cabcbe358760aaef395b67cff9336d50615279a714d0fda73682cd76ecb79a793cc67444d9c9bf84617094a8888ec415452794c573fd4763f4d007b6ed0b9144b5e44ef415a62d559594e90d3b14e9727ccd373a6ff9de79d81004f8d9438d15b170fc4e17a95792037b2f62394b48c45e42c413842e6527d2cb580abb0027d68f4737fe22aa1c78fe48b39760b3d79bf093b92f1b1e06b3466262eecf1fcec634b9f7a297c5bb1d826c8052366d43298286026e2adc9097ad1ce23343a749afa3eda75e9a46a36078fad8ec53b10425ef4c741b3080f0e202cb991edb8964064761e0b220f0bc8ddc10a2a7b6d0f3bfaf8fd330edb1214a8ae12dfe77932a5bee0e973007fa6f02912ee554616811ddc8a74543f7a51956d3435c5503ce74b10a23807d5f055df5ae3b33a4501aa45cbd1e051bb049fd59be3681e03684341ff3ef684d2ee67037d6190504e460c7f56243d1808b4653f0c257dba19729d5b181ef43ba0fe1e802c950a6a6fd6fa408e79277be3e34537fd5f5e060bd9963ac61d06f81be7bf28af8bbaa",
561+
"path": "m/44'/60'/0'/0/1",
562+
"to_address": "0x3Ef3D8bA38EBe18DB133cEc108f4D14CE00Dd9Ae",
563+
"chain_id": 8453,
564+
"nonce": "0x0",
565+
"gas_price": "0x14",
566+
"gas_limit": "0x14",
567+
"tx_type": null,
568+
"value": "0x0"
569+
},
570+
"result": {
571+
"sig_v": 16941,
572+
"sig_r": "986b2714ce571b625ddc9ed9c5ce4adb947f416ef6705c49fafbe2fbfa7b221d",
573+
"sig_s": "2580e9f1d2c7254e8ed4814ddcf81df004d3df5dda3a2325220dbad6ce50904f"
574+
}
512575
}
513576
]
514577
}

core/src/apps/ethereum/yielding.py

Lines changed: 73 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from 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
66
from .yielding_vaults import UNKNOWN_VAULT, lookup_vault
77

88
if TYPE_CHECKING:
@@ -82,6 +82,22 @@
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

86102
async 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

Comments
 (0)