You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
description: Technical specification for the Account Keychain precompile managing access keys with expiry timestampsand per-token spending limits.
2
+
description: Technical specification for the Account Keychain precompile managing access keys with expiry timestamps, spending limits, and post-T3 call-scope restrictions.
The [T3 network upgrade](/protocol/upgrades/t3) will update this specification. The sections below describe the currently active behavior. See [Upcoming changes](#upcoming-changes)for the upcoming deltas.
9
+
:::info[T3 will change this precompile]
10
+
The [T3 network upgrade](/protocol/upgrades/t3) will update this specification. The sections below describe the current T2 behavior and include `T2 -> T3 changes` notes for each section. If you are migrating now, start with [Account keychain post-T3](#account-keychain-post-t3).
11
11
:::
12
12
13
13
## Overview
14
14
15
15
The Account Keychain precompile manages authorized Access Keys for accounts, enabling Root Keys (e.g., passkeys) to provision scoped "secondary" Access Keys with expiry timestamps and per-TIP20 token spending limits.
16
16
17
+
At T3, `authorizeKey(...)` moves from top-level `expiry`, `enforceLimits`, and `limits` arguments to a `KeyRestrictions` tuple. `TokenLimit` gains `period`, so limits can be one-time or recurring, and access keys can optionally be restricted to specific targets, selectors, and recipients. Access-key-signed transactions also can no longer create contracts after T3 activation.
18
+
17
19
## Motivation
18
20
19
21
The Tempo Transaction type unlocks a number of new signature schemes, including WebAuthn (Passkeys). However, for an Account using a Passkey as its Root Key, the sender will subsequently be prompted with passkey prompts for every signature request. This can be a poor user experience for highly interactive or multi-step flows. Additionally, users would also see "Sign In" copy in prompts for signing transactions which is confusing. This proposal introduces the concept of the Root Key being able to provision a (scoped) Access Key that can be used for subsequent transactions, without the need for repetitive end-user prompting.
20
22
21
-
## Upcoming changes
23
+
### T2 -> T3 changes
24
+
25
+
At T2, the main focus is expiry and spending limits. T3 extends the same model to cover recurring budgets and explicit call scoping, which makes access keys more powerful for subscriptions, connected apps, and session-key-style flows.
26
+
27
+
## Account keychain post-T3
28
+
29
+
T3 changes the Account Keychain authorization shape on any network where the upgrade is active. Existing authorized keys continue to work. The breaking change is in how new key authorizations are encoded and what restrictions can now be enforced.
22
30
23
-
T3 updates the Account Keychain specification through [TIP-1011](/protocol/tips/tip-1011) in the following ways:
31
+
### Migration summary
24
32
25
-
-[TIP-1011](/protocol/tips/tip-1011) extends `TokenLimit` with an optional recurring `period`, so spending limits can be either one-time or periodic.
26
-
-`authorizeKey(...)` moves to the new ABI with `allowAnyCalls` and `allowedCalls`, enabling explicit call scoping during key authorization.
27
-
- New `SelectorRule` and `CallScope` structs define per-target and per-selector allowlists, including recipient-bound rules for supported TIP-20 selectors.
28
-
- New root-key-only functions `setAllowedCalls(...)` and `removeAllowedCalls(...)`, plus a new `getAllowedCalls(...)` view, are added for managing and inspecting call scopes.
29
-
-`getRemainingLimit(...)` changes to return both `remaining` and `periodEnd` so callers can observe periodic reset state.
30
-
-`updateSpendingLimit(...)` resets the remaining amount to `newLimit` but does not change the configured `period` or current `periodEnd`.
31
-
- Access-key-signed transactions can no longer create contracts in any configuration. Calling the `CREATE` opcode onchain still works.
33
+
- The legacy `authorizeKey(address,uint8,uint64,bool,(address,uint256)[])` entrypoint is no longer accepted after T3 activation. Calls to selector `0x54063a55` revert with `LegacyAuthorizeKeySelectorChanged(0x980a6025)`.
34
+
-`authorizeKey(...)` now takes a `KeyRestrictions` tuple that carries expiry, spending limits, and call scopes.
35
+
-`TokenLimit` now includes `period`, so limits can be one-time (`period = 0`) or recurring.
36
+
- Access keys can now be scoped to specific targets, selectors, and recipients.
37
+
- The precompile adds `setAllowedCalls(...)`, `removeAllowedCalls(...)`, `getAllowedCalls(...)`, and `getRemainingLimitWithPeriod(...)`.
38
+
- Access-key-signed transactions can no longer create contracts after T3 activation. Use a Root Key for deployment flows.
The T3 call must use the tuple-form signature above. A flattened seven-argument signature is not equivalent. In Foundry, that flattened form hashes to `0x203e2736`, which the precompile rejects as an unknown selector.
32
53
33
54
## Concepts
34
55
@@ -44,6 +65,10 @@ Access Keys are secondary signing keys authorized by an account's Root Key. They
44
65
- Native value transfers and `transferFrom()` are NOT limited
45
66
-**Privilege Restrictions**: Cannot authorize new keys or modify their own limits
46
67
68
+
#### T2 -> T3 changes
69
+
70
+
At T3, spending limits can recur through `TokenLimit.period`. A `period` of `0` keeps the limit one-time, while a non-zero value makes it recurring. Call scoping also becomes a first-class restriction type, and access-key-signed transactions can no longer create contracts after T3 activation.
71
+
47
72
### Authorization Hierarchy
48
73
49
74
The protocol enforces a strict hierarchy at validation time:
@@ -57,6 +82,10 @@ The protocol enforces a strict hierarchy at validation time:
57
82
- Subject to per-TIP20 token spending limits
58
83
- Can have expiry timestamps
59
84
85
+
#### T2 -> T3 changes
86
+
87
+
The hierarchy itself does not change. T3 adds new mutable call-scope management functions, and they remain Root-Key-only. Access Keys are also subject to call-scope checks during execution.
88
+
60
89
## Storage
61
90
62
91
The precompile uses a `keyId` (address) to uniquely identify each access key for an account.
@@ -72,8 +101,14 @@ The precompile uses a `keyId` (address) to uniquely identify each access key for
72
101
- byte 9: enforce_limits (bool)
73
102
- byte 10: is_revoked (bool)
74
103
104
+
### T2 -> T3 changes
105
+
106
+
At T3, `spendingLimits[...]` expands from a single remaining amount into `SpendingLimitState { remaining, max, period, periodEnd }`, and a new `keyScopes[keccak256(account || keyId)]` tree stores target, selector, and recipient allowlists. `transactionKey` remains the same.
107
+
75
108
## Interface
76
109
110
+
### T2 interface
111
+
77
112
```solidity
78
113
// SPDX-License-Identifier: MIT
79
114
pragma solidity ^0.8.13;
@@ -100,7 +135,7 @@ interface IAccountKeychain {
100
135
struct KeyInfo {
101
136
SignatureType signatureType; // Signature type of the key
At T3, `TokenLimit` gains `period`, and the interface adds `SelectorRule`, `CallScope`, and `KeyRestrictions`. `authorizeKey(...)` moves from top-level `expiry`, `enforceLimits`, and `limits` arguments to a `KeyRestrictions` tuple, while new Root-Key-only mutation methods (`setAllowedCalls(...)` and `removeAllowedCalls(...)`) and new views (`getRemainingLimitWithPeriod(...)` and `getAllowedCalls(...)`) are added around it. The legacy `getRemainingLimit(...)` selector is dropped at T3, and new T3-specific errors include `InvalidSpendingLimit()`, `ExpiryInPast()`, `SignatureTypeMismatch(uint8,uint8)`, `CallNotAllowed()`, `InvalidCallScope()`, and `LegacyAuthorizeKeySelectorChanged(bytes4)`.
function getTransactionKey() external view returns (address);
365
+
}
366
+
```
367
+
226
368
## Behavior
227
369
228
370
### Key Authorization
@@ -241,6 +383,10 @@ interface IAccountKeychain {
241
383
-`enforceLimits` determines whether spending limits are enforced for this key
242
384
-`limits` are only processed if `enforceLimits` is `true`
243
385
386
+
#### T2 -> T3 changes
387
+
388
+
At T3, the legacy selector is no longer accepted and reverts with `LegacyAuthorizeKeySelectorChanged(newSelector: 0x980a6025)`. `authorizeKey(...)` now takes `KeyRestrictions` instead of top-level `expiry`, `enforceLimits`, and `limits` arguments, `config.expiry` must be greater than the current block timestamp, duplicate token entries are invalid, `allowAnyCalls = false` with `allowedCalls = []` means scoped deny-all, and recipient-constrained selector rules are validated before state is written.
389
+
244
390
### Key Revocation
245
391
246
392
- Marks the key as revoked by setting `isRevoked` to `true` and `expiry` to `0`
@@ -252,6 +398,10 @@ interface IAccountKeychain {
252
398
- MUST be called by Root Key only (verified by checking `transactionKey[msg.sender] == 0`)
253
399
-`keyId` MUST exist (key with `expiry > 0`) (reverts with `KeyNotFound` if not found)
254
400
401
+
#### T2 -> T3 changes
402
+
403
+
Revoked keys still behave as inactive for all legacy reads. T3 also treats any stored call-scope and periodic-limit state as inaccessible once the key is revoked, and `getAllowedCalls(...)` returns scoped deny-all for revoked keys.
404
+
255
405
### Spending Limit Update
256
406
257
407
- Updates the spending limit for a specific token on an authorized key
@@ -265,6 +415,28 @@ interface IAccountKeychain {
265
415
-`keyId` MUST exist and not be revoked (reverts with `KeyNotFound` or `KeyAlreadyRevoked`)
266
416
-`keyId` MUST not be expired (reverts with `KeyExpired`)
267
417
418
+
#### T2 -> T3 changes
419
+
420
+
At T3, `newLimit` resets both `remaining` and `max` while preserving the existing `period` and current `periodEnd`. `newLimit` must also fit within TIP20's `u128` supply range.
421
+
422
+
### View Behavior
423
+
424
+
-`getKey(...)` returns key metadata.
425
+
-`getRemainingLimit(...)` returns the remaining amount for a key-token pair.
426
+
-`getTransactionKey()` returns the key used in the current transaction. `address(0)` means the Root Key.
427
+
428
+
#### T2 -> T3 changes
429
+
430
+
At T3, callers must switch from `getRemainingLimit(...)` to `getRemainingLimitWithPeriod(...)`. The legacy `getRemainingLimit(...)` selector is dropped, while `getRemainingLimitWithPeriod(...)` returns both effective `remaining` and current `periodEnd`. Missing, revoked, or expired keys return zeroed limit values, and `getAllowedCalls(...)` distinguishes unrestricted keys from scoped deny-all keys by returning `isScoped = true, scopes = []` for missing, revoked, or expired access keys.
431
+
432
+
### Allowed Call Updates
433
+
434
+
This behavior does not exist at T2.
435
+
436
+
#### T2 -> T3 changes
437
+
438
+
At T3, `setAllowedCalls(...)` creates or replaces one or more target scopes, and `removeAllowedCalls(...)` removes one stored target scope. Empty `selectorRules` means any selector on that target is allowed, while `setAllowedCalls(...)` rejects an empty scope batch, zero targets, duplicate targets, duplicate selectors, duplicate recipients, and invalid recipient-constrained rules.
439
+
268
440
## Security Considerations
269
441
270
442
### Access Key Storage
@@ -275,6 +447,10 @@ Access Keys should be securely stored to prevent unauthorized access:
275
447
-**Non-Extractable Keys**: Access Keys SHOULD be generated and stored in a non-extractable format to prevent theft. For example, use WebCrypto API with `extractable: false` when generating Keys in web browsers.
276
448
-**Secure Storage**: Private Keys MUST never be stored in plaintext. Private Keys SHOULD be encrypted and stored in a secure manner. For web applications, use browser-native secure storage mechanisms like IndexedDB with non-extractable WebCrypto keys rather than storing raw key material.
277
449
450
+
#### T2 -> T3 changes
451
+
452
+
- T3 call scopes make per-app and per-device key isolation more important, because a mis-scoped key may have a broader allowlist than intended.
453
+
278
454
### Privilege Escalation Prevention
279
455
280
456
Access Keys cannot escalate their own privileges because:
@@ -283,6 +459,10 @@ Access Keys cannot escalate their own privileges because:
283
459
3. These management functions check that `transactionKey[msg.sender] == 0` (Root Key) before executing
284
460
4. Access Keys cannot bypass this check - transactions will revert with `UnauthorizedCaller`
285
461
462
+
#### T2 -> T3 changes
463
+
464
+
The same Root-Key-only restriction applies to `setAllowedCalls(...)` and `removeAllowedCalls(...)`. On T2+ networks, mutable precompile calls also require `msg.sender == tx.origin`, which prevents contract-mediated confused-deputy patterns.
465
+
286
466
### Spending Limit Enforcement
287
467
288
468
- Spending limits are only enforced if `enforceLimits == true` for the key
@@ -295,13 +475,29 @@ Access Keys cannot escalate their own privileges because:
295
475
- Root keys (`keyId == address(0)`) have no spending limits - the function returns immediately
296
476
- Failed limit checks revert the entire transaction with `SpendingLimitExceeded`
297
477
478
+
#### T2 -> T3 changes
479
+
480
+
At T3, recurring limits roll over when `current_timestamp >= periodEnd`. Missing, revoked, or expired keys have an effective remaining limit of zero, and `getRemainingLimitWithPeriod(...)` lets callers observe rollover state directly.
481
+
482
+
### Call Scope Enforcement
483
+
484
+
This behavior does not exist at T2.
485
+
486
+
#### T2 -> T3 changes
487
+
488
+
At T3, call-scope checks run on top-level calls signed by an Access Key. If a key is scoped and a call does not match the stored target, selector, and recipient rules, execution reverts with `CallNotAllowed`, and access-key-signed transactions cannot create contracts after T3 activation.
489
+
298
490
### Key Expiry
299
491
300
492
- Keys with `expiry > 0` are checked against the current timestamp during validation
301
493
- Expired keys cause transaction rejection with `KeyExpired` error (checked via `validate_keychain_authorization()`)
302
-
-`expiry == 0` means the key never expires
494
+
-New authorizations require a future expiry timestamp
303
495
- Expiry is checked as: `current_timestamp >= expiry` (key is expired when current time reaches or exceeds expiry)
304
496
497
+
#### T2 -> T3 changes
498
+
499
+
Expired keys return zeroed limit and call-scope reads at T3.
500
+
305
501
## Usage Patterns
306
502
307
503
### First-Time Access Key Authorization
@@ -312,14 +508,26 @@ Access Keys cannot escalate their own privileges because:
312
508
4. Protocol validates Passkey signature on `key_authorization`, sets `transactionKey[account] = 0`, calls `AccountKeychain.authorizeKey()`, then validates Access Key signature
313
509
5. Transaction executes with Access Key's spending limits enforced via internal `verify_and_update_spending()`
314
510
511
+
#### T2 -> T3 changes
512
+
513
+
The same flow still applies, but the signed authorization now carries `KeyRestrictions` instead of top-level expiry and limit fields. That lets the same first-use flow provision recurring limits and call scopes.
514
+
315
515
### Subsequent Access Key Usage
316
516
317
517
1. User's Access Key signs the transaction (no `key_authorization` needed)
318
518
2. Protocol validates the Access Key via `validate_keychain_authorization()`, sets `transactionKey[account] = keyId`
319
519
3. Transaction executes with spending limit enforcement via internal `verify_and_update_spending()`
320
520
521
+
#### T2 -> T3 changes
522
+
523
+
The same flow still applies, but T3 also enforces call scopes during execution and disallows contract creation from access-key-signed transactions.
524
+
321
525
### Root Key Revoking an Access Key
322
526
323
527
1. User signs Passkey prompt → signs transaction calling `revokeKey(keyId)`
324
528
2. Transaction executes, marking the Access Key as inactive
325
529
3. Future transactions signed by that Access Key will be rejected
530
+
531
+
#### T2 -> T3 changes
532
+
533
+
The Root Key can still call `revokeKey(...)`. It can additionally call `updateSpendingLimit(...)`, `setAllowedCalls(...)`, and `removeAllowedCalls(...)` to modify restrictions after authorization, and `updateSpendingLimit(...)` now preserves the token's configured `period` and current `periodEnd`.
0 commit comments