feat(core/ethereum): allow EIP-7702 delegation under strict safety checks
What changed, and why it matters
This commit changes how Trezor hardware wallets handle a new Ethereum feature called EIP-7702, which lets users temporarily delegate control of their account to another smart contract. Previously, Trezor only allowed this delegation if the user lowered their safety settings. Now, it allows delegation under the strictest safety settings, but only for a pre-approved list of delegate addresses. Revocation (removing a delegate) was already allowed under strict settings and remains allowed. The change is framed as adding 'strict safety checks' rather than removing protections entirely.
Review the allowlist of EIP-7702 delegate addresses (lookup_eip7702_address) to confirm it is curated and cannot be bypassed, and verify that the UI confirmation flow clearly warns users about the risks of delegation. Consider whether the change warrants a changelog/security advisory entry even though the commit is marked [no changelog].
Security signals we found
Behavior change in safety check enforcement for account delegation
Allowlist-based delegate validation introduced as compensating control
Removal of explicit strict-mode prohibition for EIP-7702 authorizations
Test expectations changed from failure-under-strict to success-under-strict
Evidence from the diff
The patch removes the requirement that EIP-7702 authorizations (other than revocations) must run with safety_checks=PromptTemporarily. Instead, non-revocation delegations are now permitted under strict safety checks if the delegate address is found in a known list via lookup_eip7702_address(chain_id, delegate_bytes). Unknown delegates still raise DataError. The code no longer imports safety_checks or raises the previous ProcessError. Tests are updated to stop toggling SafetyCheckLevel and to expect signing to succeed without first triggering the strict-mode failure.
Changed components
core/src/apps/ethereum/sign_tx_eip1559.pytests/device_tests/ethereum/test_sign_eip7702.pytests/ui_tests/fixtures.jsonInspect captured patch +437 / −475
### core/src/apps/ethereum/sign_tx_eip1559.py
@@ -213,9 +213,9 @@ async def _handle_eip7702(
from trezor import TR
from trezor.ui import layouts
- from trezor.wire import DataError, ProcessError
+ from trezor.wire import DataError
- from apps.common import paths, safety_checks
+ from apps.common import paths
from .helpers import bytes_from_address, get_account_and_path
from .networks import UNKNOWN_NETWORK
@@ -252,19 +252,13 @@ async def _handle_eip7702(
delegate_addr = msg.auth7702.delegate
delegate_bytes = bytes_from_address(delegate_addr)
if delegate_bytes == b"\x00" * 20: # -> revocation
- # revocation can be done with strict safety checks
await layouts.confirm_ethereum_eip7702_revoke(
network_item=network_item,
account=account,
account_path=account_path,
nonce=nonce,
)
else:
- if safety_checks.is_strict():
- raise ProcessError(
- "EIP-7702 authorisation not allowed with strict safety checks"
- )
-
delegate_name = lookup_eip7702_address(chain_id, delegate_bytes)
if delegate_name is None:
raise DataError("Unknown EIP-7702 delegate address")
### tests/device_tests/ethereum/test_sign_eip7702.py
@@ -21,7 +21,7 @@
from trezorlib.debuglink import DebugSession as Session
from trezorlib.ethereum import decode_hex
from trezorlib.exceptions import TrezorFailure
-from trezorlib.messages import EthereumAuth7702, PaymentRequest, SafetyCheckLevel
+from trezorlib.messages import EthereumAuth7702, PaymentRequest
from trezorlib.tools import parse_path
from ...common import parametrize_using_common_fixtures
@@ -34,10 +34,6 @@
]
-def is_revocation(parameters: dict) -> bool:
- return parameters["delegate"] == "0x0000000000000000000000000000000000000000"
-
-
# Test vectors validated with Foundry
# cast wallet sign-auth $ADDRESS --mnemonic $MNEMONIC --mnemonic-derivation-path "m/44'/60'/0'/0/0" --nonce $NONCE --chain $CHAINID
# To evaluate signature parts: cast from-rlp <result_from_above>
@@ -73,20 +69,6 @@ def _sign() -> ethereum.SignTxResult:
device.apply_settings(session, experimental_features=True)
- # Revocation doesn't require disabling strict safety checks.
- if not is_revocation(parameters):
- with pytest.raises(
- TrezorFailure,
- match="ProcessError: EIP-7702 authorisation not allowed with strict safety checks",
- ):
- _sign()
-
- # Authorization requires disabling strict safety checks.
- device.apply_settings(
- session,
- safety_checks=SafetyCheckLevel.PromptTemporarily,
- )
-
res = _sign()
[auth7702_tuple] = res.auth7702_list
chain_id, delegate, nonce, y_parity, r, s = auth7702_tuple
@@ -100,11 +82,7 @@ def _sign() -> ethereum.SignTxResult:
@parametrize_using_common_fixtures("ethereum/sign_auth_eip7702_errors.json")
def test_sign_eip7702_errors(session: Session, parameters, result):
- device.apply_settings(
- session,
- safety_checks=SafetyCheckLevel.PromptTemporarily,
- experimental_features=True,
- )
+ device.apply_settings(session, experimental_features=True)
assert result["error"] # make sure it's not an empty string
with pytest.raises(TrezorFailure, match=result["error"]):
@@ -133,17 +111,7 @@ def test_sign_eip7702_errors(session: Session, parameters, result):
@parametrize_using_common_fixtures("ethereum/sign_tx_eip7702_mainnet.json")
def test_sign_eip7702_mainnet(session: Session, parameters: dict, result: dict):
- # Authorization requires disabling strict safety checks.
- if is_revocation(parameters):
- safety_checks = SafetyCheckLevel.Strict
- else:
- safety_checks = SafetyCheckLevel.PromptTemporarily
-
- device.apply_settings(
- session,
- safety_checks=safety_checks,
- experimental_features=True,
- )
+ device.apply_settings(session, experimental_features=True)
res = ethereum.sign_tx_eip1559(
session,
### tests/ui_tests/fixtures.json
[binary or diff unavailable]Why this scored 40/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.