Skip to content

Commit 6474ffc

Browse files
committed
test: add failing test for EOA admin key validation issue
This test demonstrates that when an EOA's own address is used as the publicKey for an admin key (which happens when using the EOA's private key to create the admin key), the validation fails due to a recursive validation loop in the signature checking logic. The issue occurs because: 1. unwrapAndValidateSignature extracts the inner signature 2. For Secp256k1 keys, it calls isValidSignatureNowCalldata with the EOA address 3. Since the EOA has code (via EIP-7702), it calls isValidSignature on the EOA 4. This triggers the 64/65 byte special case expecting raw EOA signature 5. But the signature is EIP-712 formatted, causing validation to fail
1 parent e84fe99 commit 6474ffc

File tree

1 file changed

+63
-0
lines changed

1 file changed

+63
-0
lines changed

test/EOAKeyConflict.t.sol

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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+
}

0 commit comments

Comments
 (0)