fix(ethereum): claim.xyz address byte comparision.
What changed, and why it matters
This patch fixes a bug in how Trezor's Ethereum app checked whether a transaction was meant for the claim.xyz rewards distributor. Previously it compared a lowercase hex string (like '0xabc...') against raw address bytes, which would never match. The fix compares raw bytes to raw bytes. The practical effect is that the special claim.xyz user interface and safety checks now actually trigger for the intended contract, instead of silently being skipped for all transactions.
Treat this as a correctness fix with possible security UX implications. Review whether the bypass could have allowed misleading reward-claim prompts or suppressed safety prompts, and consider whether a firmware update note is warranted for users who rely on the claim.xyz flow. No independent researcher attribution or CVE is present in the materials.
Security signals we found
Incorrect type/string-vs-bytes comparison caused intended security/UX gate to be bypassed
Special claim flow safety checks (e.g., non-zero ETH value rejection, signer equality, reward definition fetching cap) were not reliably applied to the intended contract
Patch converts address constant to raw bytes and compares bytes to bytes
No changelog entry suggests routine fix rather than disclosed security incident
Evidence from the diff
In core/src/apps/ethereum/yielding.py, the code that decides whether to use the custom claim.xyz (Merkl) flow compared msg.to.lower() (a lowercase hex string) to _MERKL_XYZ_CLAIM_DISTRIBUTOR_ADDR, which was previously defined as the ASCII string ‘0x3ef3…’. This comparison is always false because one side is a hex string and the other is raw 20-byte address data. The patch moves the address constant to the top of the module as raw bytes, adds a debug assertion verifying it matches the unhexlified address, and changes the routing condition to compare address_bytes == _MERKL_XYZ_CLAIM_DISTRIBUTOR_ADDR. It also removes the now-redundant string-based check inside _prepare_merkl_claim.
Changed components
Trezor firmware Ethereum applicationcore/src/apps/ethereum/yielding.pyclaim.xyz / Merkl reward-claim transaction handlingInspect captured patch +16 / −8
diff --git a/core/src/apps/ethereum/yielding.py b/core/src/apps/ethereum/yielding.py
index 03e97210..cfec6da0 100644
--- a/core/src/apps/ethereum/yielding.py
+++ b/core/src/apps/ethereum/yielding.py
@@ -22,7 +22,13 @@ FUNC_SIG_WITHDRAW = b"\xb4\x60\xaf\x94"
FUNC_SIG_REDEEM = b"\xba\x08\x76\x52"
FUNC_SIG_CLAIM = b"\x71\xee\x95\xc0"
+_MERKL_XYZ_CLAIM_DISTRIBUTOR_ADDR = (
+ b"\x3e\xf3\xd8\xba\x38\xeb\xe1\x8d\xb1\x33\xce\xc1\x08\xf4\xd1\x4c\xe0\x0d\xd9\xae"
+)
+
if __debug__:
+ from ubinascii import unhexlify
+
from trezor.crypto.hashlib import sha3_256
assert (
@@ -43,6 +49,10 @@ if __debug__:
b"claim(address[],address[],uint256[],bytes32[][])", keccak=True
).digest()[:4]
)
+ # https://etherscan.io/address/0x3ef3d8ba38ebe18db133cec108f4d14ce00dd9ae
+ assert _MERKL_XYZ_CLAIM_DISTRIBUTOR_ADDR == unhexlify(
+ "3ef3d8ba38ebe18db133cec108f4d14ce00dd9ae"
+ )
# deposit(uint256 assets, address receiver)
DEPOSIT_DISPLAY_FORMAT = DisplayFormat(
@@ -146,7 +156,10 @@ async def get_approver(
vault=vault,
token=token,
)
- elif func_sig == FUNC_SIG_CLAIM:
+ elif (
+ func_sig == FUNC_SIG_CLAIM
+ and address_bytes == _MERKL_XYZ_CLAIM_DISTRIBUTOR_ADDR
+ ):
handler = await _prepare_merkl_claim(
calldata=calldata,
msg=msg,
@@ -230,8 +243,6 @@ async def _prepare_merkl_claim(
from .layout import require_confirm_claim_rewards
from .yielding_vaults import get_token_label
- _MERKL_XYZ_CLAIM_DISTRIBUTOR_ADDR = "0x3ef3d8ba38ebe18db133cec108f4d14ce00dd9ae"
-
if int.from_bytes(msg.value, "big") != 0:
raise DataError(
"Non-zero ETH transfer with claim rewards transaction not allowed"
@@ -284,11 +295,8 @@ async def _prepare_merkl_claim(
if other != first_user:
return None
- # We don't show claim flows for non claim.xyz or for non-signer users.
- if (
- msg.to.lower() != _MERKL_XYZ_CLAIM_DISTRIBUTOR_ADDR
- or sender_bytes != first_user
- ):
+ # We don't show claim flows for non-signer users.
+ if sender_bytes != first_user:
return None
# Not sure about the UX if we fetch too many defintions so capping definition fetching to 4 for now.
Why this scored 61/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.