Skip to content

Commit 0576eef

Browse files
authored
Readd revoke, explicit eoa verifier (#10)
1 parent 6ef3197 commit 0576eef

4 files changed

Lines changed: 207 additions & 15 deletions

File tree

src/AccountConfiguration.sol

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ contract AccountConfiguration is IAccountConfiguration {
6363
/// @notice Owner can change account owners
6464
uint8 public constant SCOPE_CHANGE_OWNERS = 0x08;
6565

66+
/// @notice Sentinel verifier written on self-ownerId revocation to block implicit re-authorization.
67+
address public constant REVOKED_VERIFIER = address(1);
68+
6669
// ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡
6770
// STORAGE
6871
// ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡
@@ -236,7 +239,10 @@ contract AccountConfiguration is IAccountConfiguration {
236239
}
237240

238241
function isOwner(address account, bytes32 ownerId) public view returns (bool) {
239-
return _ownerConfig[ownerId][account].verifier != address(0);
242+
address verifier = _ownerConfig[ownerId][account].verifier;
243+
if (verifier > REVOKED_VERIFIER) return true;
244+
// Implicit EOA: self-ownerId with truly empty slot
245+
return verifier == address(0) && ownerId == bytes32(bytes20(account));
240246
}
241247

242248
function getOwnerConfig(address account, bytes32 ownerId) external view returns (OwnerConfig memory) {
@@ -301,11 +307,9 @@ contract AccountConfiguration is IAccountConfiguration {
301307
}
302308

303309
function _authorizeOwner(address account, bytes32 ownerId, OwnerConfig memory config) internal nonZero(account) {
304-
// Must be legitimate verifier
305-
require(config.verifier != address(0));
306-
307-
// Must not already be registered
308-
require(_ownerConfig[ownerId][account].verifier == address(0));
310+
require(config.verifier > REVOKED_VERIFIER);
311+
address existing = _ownerConfig[ownerId][account].verifier;
312+
require(existing == address(0) || existing == REVOKED_VERIFIER);
309313

310314
_ownerConfig[ownerId][account] = config;
311315
emit OwnerAuthorized(account, ownerId, config);
@@ -314,8 +318,7 @@ contract AccountConfiguration is IAccountConfiguration {
314318
function _revokeOwner(address account, bytes32 ownerId) internal nonZero(account) {
315319
require(isOwner(account, ownerId));
316320
if (ownerId == bytes32(bytes20(account))) {
317-
// Self-ownerId: sentinel prevents protocol implicit re-authorization on empty slot
318-
_ownerConfig[ownerId][account] = OwnerConfig({verifier: address(0), scopes: type(uint8).max});
321+
_ownerConfig[ownerId][account] = OwnerConfig({verifier: REVOKED_VERIFIER, scopes: 0});
319322
} else {
320323
delete _ownerConfig[ownerId][account];
321324
}
@@ -369,6 +372,8 @@ contract AccountConfiguration is IAccountConfiguration {
369372
view
370373
returns (uint8 scopes)
371374
{
375+
if (verifier == address(0)) return _verifyImplicitEOA(account, hash, data);
376+
372377
bytes32 ownerId = IVerifier(verifier).verify(hash, data);
373378
require(ownerId != bytes32(0));
374379

@@ -377,6 +382,17 @@ contract AccountConfiguration is IAccountConfiguration {
377382
return config.scopes;
378383
}
379384

385+
/// @dev Implicit EOA: native ecrecover, requires self-ownerId slot is empty (not REVOKED_VERIFIER).
386+
function _verifyImplicitEOA(address account, bytes32 hash, bytes calldata data) internal view returns (uint8) {
387+
require(_ownerConfig[bytes32(bytes20(account))][account].verifier == address(0));
388+
require(data.length == 65);
389+
bytes32 r = bytes32(data[:32]);
390+
bytes32 s = bytes32(data[32:64]);
391+
address recovered = ecrecover(hash, uint8(data[64]), r, s);
392+
require(recovered == account);
393+
return 0;
394+
}
395+
380396
// ----------------------------------------------------------------------------------------------------------------
381397
// ACCOUNT CREATION
382398
// ----------------------------------------------------------------------------------------------------------------

test/lib/AccountConfigurationTest.sol

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ contract AccountConfigurationTest is Test {
7878
return abi.encodePacked(address(k1Verifier), sig);
7979
}
8080

81+
/// @dev Build auth bytes for implicit EOA path: address(0) || ecdsa signature.
82+
function _buildImplicitEOAAuth(uint256 pk, bytes32 digest) internal pure returns (bytes memory) {
83+
(uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, digest);
84+
return abi.encodePacked(address(0), r, s, v);
85+
}
86+
8187
// ── Canonical digest computation ──
8288

8389
function _computeOwnerChangeBatchDigest(

test/unit/AccountConfiguration/applyKeyChange.t.sol

Lines changed: 112 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -262,12 +262,92 @@ contract ApplyConfigChangeOwnerTest is AccountConfigurationTest {
262262
accountConfiguration.applySignedOwnerChanges(account, uint64(block.chainid), changes, badAuth);
263263
}
264264

265-
// ── EOA self-ownerId (default key) revoke/add ──
265+
// ── Implicit EOA (registered by default) ──
266+
//
267+
// Every account has an implicit self-ownerId bytes32(bytes20(account))
268+
// that is authorized with unrestricted scopes when the config slot
269+
// is empty. No createAccount/importAccount needed.
270+
271+
function test_implicitEOA_canSignOwnerChanges() public {
272+
uint256 eoaPk = 500;
273+
address eoa = vm.addr(eoaPk);
274+
bytes32 newOwnerId = bytes32(bytes20(vm.addr(501)));
275+
276+
IAccountConfiguration.OwnerChange[] memory changes = new IAccountConfiguration.OwnerChange[](1);
277+
changes[0] = IAccountConfiguration.OwnerChange({
278+
ownerId: newOwnerId,
279+
changeType: 0x01,
280+
configData: abi.encode(IAccountConfiguration.OwnerConfig({verifier: address(k1Verifier), scopes: 0x00}))
281+
});
282+
283+
uint64 seq = accountConfiguration.getChangeSequences(eoa).local;
284+
bytes32 digest = _computeOwnerChangeBatchDigest(eoa, uint64(block.chainid), seq, changes);
285+
bytes memory auth = _buildImplicitEOAAuth(eoaPk, digest);
286+
287+
accountConfiguration.applySignedOwnerChanges(eoa, uint64(block.chainid), changes, auth);
288+
assertTrue(accountConfiguration.isOwner(eoa, newOwnerId));
289+
}
290+
291+
function test_implicitEOA_canRevokeItselfViaSentinel() public {
292+
uint256 eoaPk = 500;
293+
address eoa = vm.addr(eoaPk);
294+
bytes32 selfOwnerId = bytes32(bytes20(eoa));
295+
296+
assertTrue(accountConfiguration.isOwner(eoa, selfOwnerId));
297+
298+
// Add a second key first using implicit EOA auth
299+
bytes32 newOwnerId = bytes32(bytes20(vm.addr(501)));
300+
_implicitAuthorizeOwner(eoa, eoaPk, newOwnerId, address(k1Verifier));
301+
302+
// Revoke self-ownerId using the new explicit key
303+
_revokeOwner(eoa, 501, selfOwnerId);
304+
305+
assertFalse(accountConfiguration.isOwner(eoa, selfOwnerId));
306+
assertTrue(accountConfiguration.isOwner(eoa, newOwnerId));
307+
308+
IAccountConfiguration.OwnerConfig memory cfg = accountConfiguration.getOwnerConfig(eoa, selfOwnerId);
309+
assertEq(cfg.verifier, accountConfiguration.REVOKED_VERIFIER());
310+
assertEq(cfg.scopes, 0);
311+
}
312+
313+
function test_implicitEOA_canBeExplicitlyRegistered() public {
314+
uint256 eoaPk = 500;
315+
address eoa = vm.addr(eoaPk);
316+
bytes32 selfOwnerId = bytes32(bytes20(eoa));
317+
318+
_implicitAuthorizeOwnerWithScope(eoa, eoaPk, selfOwnerId, address(k1Verifier), 0x01);
319+
320+
IAccountConfiguration.OwnerConfig memory cfg = accountConfiguration.getOwnerConfig(eoa, selfOwnerId);
321+
assertEq(cfg.verifier, address(k1Verifier));
322+
assertEq(cfg.scopes, 0x01);
323+
}
324+
325+
function test_implicitEOA_crossChainOwnerChange() public {
326+
uint256 eoaPk = 500;
327+
address eoa = vm.addr(eoaPk);
328+
bytes32 newOwnerId = bytes32(bytes20(vm.addr(501)));
329+
330+
IAccountConfiguration.OwnerChange[] memory changes = new IAccountConfiguration.OwnerChange[](1);
331+
changes[0] = IAccountConfiguration.OwnerChange({
332+
ownerId: newOwnerId,
333+
changeType: 0x01,
334+
configData: abi.encode(IAccountConfiguration.OwnerConfig({verifier: address(k1Verifier), scopes: 0x00}))
335+
});
336+
337+
// chainId=0 for multichain
338+
uint64 seq = accountConfiguration.getChangeSequences(eoa).multichain;
339+
bytes32 digest = _computeOwnerChangeBatchDigest(eoa, 0, seq, changes);
340+
bytes memory auth = _buildImplicitEOAAuth(eoaPk, digest);
341+
342+
accountConfiguration.applySignedOwnerChanges(eoa, 0, changes, auth);
343+
assertTrue(accountConfiguration.isOwner(eoa, newOwnerId));
344+
}
345+
346+
// ── EOA self-ownerId revoke/add with explicit registration ──
266347
//
267348
// The self-ownerId for an account is bytes32(bytes20(account)).
268349
// Revoking this ownerId sets a sentinel (verifier=0, scopes=0xff)
269-
// instead of deleting, to prevent the 8130 protocol's implicit
270-
// re-authorization on an empty slot.
350+
// instead of deleting, to block the implicit authorization.
271351

272352
function test_selfOwnerId_addKey() public {
273353
(address account,) = _createK1Account(OWNER_PK);
@@ -289,8 +369,8 @@ contract ApplyConfigChangeOwnerTest is AccountConfigurationTest {
289369
assertFalse(accountConfiguration.isOwner(account, selfOwnerId));
290370

291371
IAccountConfiguration.OwnerConfig memory cfg = accountConfiguration.getOwnerConfig(account, selfOwnerId);
292-
assertEq(cfg.verifier, address(0));
293-
assertEq(cfg.scopes, type(uint8).max);
372+
assertEq(cfg.verifier, accountConfiguration.REVOKED_VERIFIER());
373+
assertEq(cfg.scopes, 0);
294374
}
295375

296376
function test_selfOwnerId_canReauthorizeAfterSentinel() public {
@@ -334,8 +414,8 @@ contract ApplyConfigChangeOwnerTest is AccountConfigurationTest {
334414

335415
// Self-ownerId has sentinel, not zeroed
336416
IAccountConfiguration.OwnerConfig memory cfg = accountConfiguration.getOwnerConfig(account, selfOwnerId);
337-
assertEq(cfg.verifier, address(0));
338-
assertEq(cfg.scopes, type(uint8).max);
417+
assertEq(cfg.verifier, accountConfiguration.REVOKED_VERIFIER());
418+
assertEq(cfg.scopes, 0);
339419
}
340420

341421
function test_selfOwnerId_revokedCannotSignOwnerChanges() public {
@@ -442,6 +522,31 @@ contract ApplyConfigChangeOwnerTest is AccountConfigurationTest {
442522
accountConfiguration.applySignedOwnerChanges(account, uint64(block.chainid), changes, auth);
443523
}
444524

525+
function _implicitAuthorizeOwner(address account, uint256 pk, bytes32 newOwnerId, address verifier) internal {
526+
_implicitAuthorizeOwnerWithScope(account, pk, newOwnerId, verifier, 0x00);
527+
}
528+
529+
function _implicitAuthorizeOwnerWithScope(
530+
address account,
531+
uint256 pk,
532+
bytes32 newOwnerId,
533+
address verifier,
534+
uint8 scope
535+
) internal {
536+
IAccountConfiguration.OwnerChange[] memory changes = new IAccountConfiguration.OwnerChange[](1);
537+
changes[0] = IAccountConfiguration.OwnerChange({
538+
ownerId: newOwnerId,
539+
changeType: 0x01,
540+
configData: abi.encode(IAccountConfiguration.OwnerConfig({verifier: verifier, scopes: scope}))
541+
});
542+
543+
uint64 seq = accountConfiguration.getChangeSequences(account).local;
544+
bytes32 digest = _computeOwnerChangeBatchDigest(account, uint64(block.chainid), seq, changes);
545+
bytes memory auth = _buildImplicitEOAAuth(pk, digest);
546+
547+
accountConfiguration.applySignedOwnerChanges(account, uint64(block.chainid), changes, auth);
548+
}
549+
445550
function _lockAccount(address account) internal {
446551
vm.prank(account);
447552
accountConfiguration.lock(1 hours);

test/unit/AccountConfiguration/verify.t.sol

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,56 @@ contract VerifyTest is AccountConfigurationTest {
115115
assertEq(scopes, uint8(0x00));
116116
}
117117

118+
// ── Implicit EOA (registered by default) ──
119+
120+
function test_verify_implicitEOA() public {
121+
uint256 eoaPk = 500;
122+
address eoa = vm.addr(eoaPk);
123+
124+
bytes32 hash = keccak256("implicit eoa verify");
125+
bytes memory auth = _buildImplicitEOAAuth(eoaPk, hash);
126+
127+
// No createAccount or importAccount — the EOA is implicitly authorized
128+
uint8 scopes = accountConfiguration.verify(eoa, hash, auth);
129+
assertEq(scopes, 0);
130+
}
131+
132+
function test_verify_implicitEOA_isOwnerReturnsTrue() public {
133+
uint256 eoaPk = 500;
134+
address eoa = vm.addr(eoaPk);
135+
136+
assertTrue(accountConfiguration.isOwner(eoa, bytes32(bytes20(eoa))));
137+
}
138+
139+
function test_verify_implicitEOA_nonSelfOwnerIdNotImplicit() public {
140+
uint256 eoaPk = 500;
141+
address eoa = vm.addr(eoaPk);
142+
143+
// A random ownerId that isn't bytes32(bytes20(eoa)) should NOT be implicit
144+
bytes32 randomOwnerId = bytes32(bytes20(vm.addr(999)));
145+
assertFalse(accountConfiguration.isOwner(eoa, randomOwnerId));
146+
}
147+
148+
function test_verify_implicitEOA_revokedBySentinel() public {
149+
uint256 eoaPk = 500;
150+
address eoa = vm.addr(eoaPk);
151+
bytes32 selfOwnerId = bytes32(bytes20(eoa));
152+
153+
// Implicit EOA signs to add a second key
154+
bytes32 newOwnerId = bytes32(bytes20(vm.addr(501)));
155+
_implicitAuthorizeOwner(eoa, eoaPk, newOwnerId, address(k1Verifier));
156+
157+
// Revoke the self-ownerId (writes sentinel) using the new explicit key
158+
_revokeOwner(eoa, 501, selfOwnerId);
159+
160+
assertFalse(accountConfiguration.isOwner(eoa, selfOwnerId));
161+
162+
// Implicit path is now blocked
163+
bytes32 hash = keccak256("after sentinel");
164+
vm.expectRevert();
165+
accountConfiguration.verify(eoa, hash, _buildImplicitEOAAuth(eoaPk, hash));
166+
}
167+
118168
// ── Helpers ──
119169

120170
function _authorizeOwner(address account, uint256 pk, bytes32 newOwnerId, address verifier) internal {
@@ -148,4 +198,19 @@ contract VerifyTest is AccountConfigurationTest {
148198

149199
accountConfiguration.applySignedOwnerChanges(account, uint64(block.chainid), changes, auth);
150200
}
201+
202+
function _implicitAuthorizeOwner(address account, uint256 pk, bytes32 newOwnerId, address verifier) internal {
203+
IAccountConfiguration.OwnerChange[] memory changes = new IAccountConfiguration.OwnerChange[](1);
204+
changes[0] = IAccountConfiguration.OwnerChange({
205+
ownerId: newOwnerId,
206+
changeType: 0x01,
207+
configData: abi.encode(IAccountConfiguration.OwnerConfig({verifier: verifier, scopes: 0x00}))
208+
});
209+
210+
uint64 seq = accountConfiguration.getChangeSequences(account).local;
211+
bytes32 digest = _computeOwnerChangeBatchDigest(account, uint64(block.chainid), seq, changes);
212+
bytes memory auth = _buildImplicitEOAAuth(pk, digest);
213+
214+
accountConfiguration.applySignedOwnerChanges(account, uint64(block.chainid), changes, auth);
215+
}
151216
}

0 commit comments

Comments
 (0)