1+ // SPDX-License-Identifier: MIT
2+ pragma solidity ^ 0.8.4 ;
3+
4+ import "./Base.t.sol " ;
5+
6+ /// @dev Test that reproduces the issue where using the EOA's private key as an admin key
7+ /// causes validation failure due to recursive validation loop.
8+ contract EOAKeyConflictTest is BaseTest {
9+
10+ /// @dev Test that using EOA's own address as admin key publicKey causes validation failure
11+ function testEOAAsAdminKeyFails () public {
12+ // Create a delegated EOA
13+ DelegatedEOA memory d = _randomEIP7702DelegatedEOA ();
14+ vm.deal (d.eoa, 100 ether);
15+
16+ // Create an admin key where the publicKey is the EOA's own address
17+ // This simulates what happens when using mock_admin_with_key(KeyType::Secp256k1, eoa_private_key)
18+ PassKey memory adminKey;
19+ adminKey.k.keyType = IthacaAccount.KeyType.Secp256k1;
20+ adminKey.k.publicKey = abi.encode (d.eoa); // EOA's address as publicKey
21+ adminKey.k.isSuperAdmin = true ;
22+ adminKey.k.expiry = 0 ;
23+ adminKey.privateKey = d.privateKey; // Same private key as EOA
24+ adminKey.keyHash = _hash (adminKey.k);
25+
26+ // Authorize the key
27+ vm.prank (d.eoa);
28+ d.d.authorize (adminKey.k);
29+
30+ // Create a simple call
31+ ERC7821 .Call[] memory calls = new ERC7821 .Call [](1 );
32+ calls[0 ] = _thisTargetFunctionCall (0 , hex "" );
33+
34+ // Get nonce and compute digest
35+ uint256 nonce = d.d.getNonce (0 );
36+ bytes32 digest = d.d.computeDigest (calls, nonce);
37+
38+ // Sign with the admin key (using EOA's private key)
39+ (uint8 v , bytes32 r , bytes32 s ) = vm.sign (adminKey.privateKey, digest);
40+ bytes memory innerSignature = abi.encodePacked (r, s, v);
41+
42+ // Wrap the signature with keyHash and prehash flag (as relay.rs does)
43+ bytes memory wrappedSignature = abi.encodePacked (
44+ innerSignature,
45+ adminKey.keyHash,
46+ uint8 (0 ) // prehash = false
47+ );
48+
49+ // Try to execute with the wrapped signature
50+ bytes memory opData = abi.encodePacked (nonce, wrappedSignature);
51+ bytes memory executionData = abi.encode (calls, opData);
52+
53+ // This should fail because:
54+ // 1. unwrapAndValidateSignature extracts the 65-byte inner signature
55+ // 2. For Secp256k1 keys, it calls SignatureCheckerLib.isValidSignatureNowCalldata
56+ // 3. Since publicKey is the EOA's address and EOA has code (delegated),
57+ // it calls isValidSignature on the EOA
58+ // 4. This triggers the 64/65 byte special case which expects ecrecover to return EOA address
59+ // 5. But ecrecover returns a different address because the digest is EIP-712 formatted
60+ vm.expectRevert (bytes4 (keccak256 ("Unauthorized() " )));
61+ d.d.execute (_ERC7821_BATCH_EXECUTION_MODE, executionData);
62+ }
63+
64+ /// @dev Test that using a different key works correctly
65+ function testDifferentAdminKeyWorks () public {
66+ // Create a delegated EOA
67+ DelegatedEOA memory d = _randomEIP7702DelegatedEOA ();
68+ vm.deal (d.eoa, 100 ether);
69+
70+ // Create an admin key with a DIFFERENT address/private key
71+ PassKey memory adminKey = _randomSecp256k1PassKey ();
72+ adminKey.k.isSuperAdmin = true ;
73+
74+ // Authorize the key
75+ vm.prank (d.eoa);
76+ d.d.authorize (adminKey.k);
77+
78+ // Create a simple call
79+ ERC7821 .Call[] memory calls = new ERC7821 .Call [](1 );
80+ calls[0 ] = _thisTargetFunctionCall (0 , hex "" );
81+
82+ // Get nonce and compute digest
83+ uint256 nonce = d.d.getNonce (0 );
84+ bytes32 digest = d.d.computeDigest (calls, nonce);
85+
86+ // Sign with the admin key
87+ (uint8 v , bytes32 r , bytes32 s ) = vm.sign (adminKey.privateKey, digest);
88+ bytes memory innerSignature = abi.encodePacked (r, s, v);
89+
90+ // Wrap the signature with keyHash and prehash flag
91+ bytes memory wrappedSignature = abi.encodePacked (
92+ innerSignature,
93+ adminKey.keyHash,
94+ uint8 (0 ) // prehash = false
95+ );
96+
97+ // Execute with the wrapped signature
98+ bytes memory opData = abi.encodePacked (nonce, wrappedSignature);
99+ bytes memory executionData = abi.encode (calls, opData);
100+
101+ // This should succeed because the admin key's publicKey is NOT the EOA's address
102+ d.d.execute (_ERC7821_BATCH_EXECUTION_MODE, executionData);
103+
104+ // Verify the call was executed
105+ assertEq (targetFunctionPayloads.length , 1 );
106+ assertEq (targetFunctionPayloads[0 ].by, d.eoa);
107+ }
108+
109+ /// @dev Test that demonstrates the exact validation flow when EOA is used as admin key
110+ function testValidationFlowWithEOAKey () public {
111+ // Create a delegated EOA
112+ DelegatedEOA memory d = _randomEIP7702DelegatedEOA ();
113+
114+ // Create an admin key where publicKey is EOA's address
115+ PassKey memory adminKey;
116+ adminKey.k.keyType = IthacaAccount.KeyType.Secp256k1;
117+ adminKey.k.publicKey = abi.encode (d.eoa);
118+ adminKey.k.isSuperAdmin = true ;
119+ adminKey.privateKey = d.privateKey;
120+ adminKey.keyHash = _hash (adminKey.k);
121+
122+ // Authorize the key
123+ vm.prank (d.eoa);
124+ d.d.authorize (adminKey.k);
125+
126+ // Create a digest for testing
127+ bytes32 digest = keccak256 ("test message " );
128+
129+ // Sign with the admin key
130+ (uint8 v , bytes32 r , bytes32 s ) = vm.sign (adminKey.privateKey, digest);
131+ bytes memory innerSignature = abi.encodePacked (r, s, v);
132+
133+ // Test unwrapAndValidateSignature directly with wrapped signature
134+ bytes memory wrappedSignature = abi.encodePacked (
135+ innerSignature,
136+ adminKey.keyHash,
137+ uint8 (0 )
138+ );
139+
140+ // This will fail because:
141+ // 1. It unwraps to get 65-byte innerSignature
142+ // 2. For Secp256k1, calls isValidSignatureNowCalldata(d.eoa, digest, innerSignature)
143+ // 3. Since d.eoa has code, it calls isValidSignature on d.eoa
144+ // 4. That hits the 65-byte special case, tries ecrecover
145+ // 5. ecrecover doesn't return d.eoa because the signature is for a different digest context
146+ (bool isValid , bytes32 returnedKeyHash ) = d.d.unwrapAndValidateSignature (digest, wrappedSignature);
147+
148+ assertFalse (isValid, "Validation should fail when EOA is used as admin key " );
149+ assertEq (returnedKeyHash, adminKey.keyHash, "Should return the correct keyHash even on failure " );
150+ }
151+ }
0 commit comments