Account Keychain Precompile
Address: 0xAAAAAAAA00000000000000000000000000000000
Account Keychain overview
The Account Keychain precompile manages authorized Access Keys for accounts, enabling Root Keys (e.g., passkeys) and admin access keys to provision secondary keys. Limited access keys can have expiry timestamps, recurring or one-time per-TIP20 token spending limits, and explicit call scopes that restrict which targets, selectors, and recipients an Access Key may invoke.
authorizeKey(...) takes a KeyRestrictions tuple that bundles expiry, spending limits, and call scopes. authorizeAdminKey(...) provisions an admin key for account key management. Access-key-signed transactions cannot create contracts; use a Root Key for deployment flows.
Motivation
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. T6 extends this model with admin access keys for key management. Recurring spending budgets and explicit call scoping make limited keys suitable for subscriptions, connected apps, and session-key-style flows.
Concepts
Access Keys
Access Keys are secondary signing keys authorized by an account's Root Key or an admin key. Limited access keys can sign transactions on behalf of the account with the following restrictions:
- Expiry: Unix timestamp when the key becomes invalid. A non-expiring
key_authorizationomitsexpiryat the transaction RLP layer; the protocol translates that omission tou64::MAXbefore calling the precompile. Direct precompile callers should passu64::MAXfor a non-expiring key. Literal0is treated as past expiry and rejected. - Spending Limits: Per-TIP20 token limits that deplete as tokens are spent
- Limits are one-time (
period = 0) or recurring (period > 0, in seconds). Recurring limits roll over tomaxwhencurrent_timestamp >= periodEnd. - Limits can be updated by the Root Key or an active admin access key via
updateSpendingLimit(). An update resetsremainingandmaxtonewLimitbut preserves the configuredperiodand currentperiodEnd. - Spending limits only apply to TIP20
transfer(),transferWithMemo(), andapprove()calls - Spending limits only apply when
msg.sender == tx.origin(direct EOA calls, not contract calls) - Native value transfers and
transferFrom()are NOT limited
- Limits are one-time (
- Call Scopes: An Access Key is either unrestricted (
allowAnyCalls = true) or restricted to an explicit allowlist of(target, selector, recipients)tuples. An empty allowlist withallowAnyCalls = falsemeans scoped deny-all. - No Contract Creation: Access-key-signed transactions cannot perform
CREATEorCREATE2, including via factory contracts. Use a Root Key for deployments. - Privilege Restrictions: Limited access keys cannot authorize new keys or modify their own limits or scopes. Admin access keys can manage keys, but cannot carry spending limits, call scopes, or expiry.
Authorization Hierarchy
The protocol enforces a strict hierarchy at validation time:
-
Root Key: The account's main key (derived from the account address)
- Can call all precompile functions, including the key-management mutators (
authorizeKey,authorizeAdminKey,revokeKey,updateSpendingLimit,setAllowedCalls,removeAllowedCalls) - Has no spending limits or call-scope restrictions
- Can call all precompile functions, including the key-management mutators (
-
Admin Access Keys: Secondary authorized keys for account administration
- Can call key-management mutators, including
authorizeKey,authorizeAdminKey, andrevokeKey - Cannot carry spending limits, call scopes, or expiry
- Cannot create contracts
- Can call key-management mutators, including
-
Limited Access Keys: Secondary authorized keys for scoped transactions
- Cannot call mutable precompile functions (only view functions are allowed)
- Subject to per-TIP20 token spending limits
- Subject to call-scope checks during execution
- Cannot create contracts
- Can have expiry timestamps
Storage
The precompile uses a keyId (address) to uniquely identify each access key for an account.
Storage Mappings:
keys[account][keyId]→ PackedAuthorizedKeystruct (signature type, expiry, enforce_limits, is_revoked, is_admin)spendingLimits[keccak256(account || keyId)][token]→SpendingLimitState { remaining, max, period, periodEnd }keyScopes[keccak256(account || keyId)]→ Tree of(target, selector, recipients)allowlists used during call-scope checkstransactionKey→ Transient storage for the key ID that signed the current transaction (slot 0)
AuthorizedKey Storage Layout (packed into single slot):
- byte 0: signature_type (u8)
- bytes 1-8: expiry (u64, little-endian)
- byte 9: enforce_limits (bool)
- byte 10: is_revoked (bool)
- byte 11: is_admin (bool)
Interface
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
interface IAccountKeychain {
enum SignatureType {
Secp256k1,
P256,
WebAuthn
}
struct TokenLimit {
address token;
uint256 amount;
uint64 period;
}
struct SelectorRule {
bytes4 selector;
address[] recipients;
}
struct CallScope {
address target;
SelectorRule[] selectorRules;
}
struct KeyRestrictions {
uint64 expiry;
bool enforceLimits;
TokenLimit[] limits;
bool allowAnyCalls;
CallScope[] allowedCalls;
}
struct KeyInfo {
SignatureType signatureType;
address keyId;
uint64 expiry;
bool enforceLimits;
bool isRevoked;
}
event KeyAuthorized(address indexed account, address indexed publicKey, uint8 signatureType, uint64 expiry);
event AdminKeyAuthorized(address indexed account, address indexed publicKey);
event KeyRevoked(address indexed account, address indexed publicKey);
event SpendingLimitUpdated(address indexed account, address indexed publicKey, address indexed token, uint256 newLimit);
event AccessKeySpend(address indexed account, address indexed publicKey, address indexed token, uint256 amount, uint256 remainingLimit);
error UnauthorizedCaller();
error KeyAlreadyExists();
error KeyNotFound();
error KeyExpired();
error SpendingLimitExceeded();
error InvalidSpendingLimit();
error InvalidSignatureType();
error ZeroPublicKey();
error ExpiryInPast();
error KeyAlreadyRevoked();
error SignatureTypeMismatch(uint8 expected, uint8 actual);
error CallNotAllowed();
error InvalidCallScope();
error InvalidKeyId();
error LegacyAuthorizeKeySelectorChanged(bytes4 newSelector);
function authorizeKey(
address keyId,
SignatureType signatureType,
KeyRestrictions calldata config
) external;
function revokeKey(address keyId) external;
function authorizeAdminKey(
address keyId,
SignatureType signatureType,
bytes32 witness
) external;
function updateSpendingLimit(
address keyId,
address token,
uint256 newLimit
) external;
function setAllowedCalls(
address keyId,
CallScope[] calldata scopes
) external;
function removeAllowedCalls(address keyId, address target) external;
function getKey(address account, address keyId) external view returns (KeyInfo memory);
function getRemainingLimitWithPeriod(
address account,
address keyId,
address token
) external view returns (uint256 remaining, uint64 periodEnd);
function getAllowedCalls(
address account,
address keyId
) external view returns (bool isScoped, CallScope[] memory scopes);
function isAdminKey(address account, address keyId) external view returns (bool);
function getTransactionKey() external view returns (address);
}Behavior
Key Authorization
key_authorizationincludes an optional trailingwitness: bytes32field. When present, the witness is included in the signing hash, checked against the account's burned-witness set, and emitted when the key authorization is registered. The protocol otherwise treats it as opaque and application-defined, so apps can bind a single access-key authorization signature to an offchain challenge.
- Creates a new key entry with the specified
signatureType,config.expiry,config.enforceLimits, andisRevokedset tofalse - If
enforceLimitsistrue, initializes spending limits for each specified token. EachTokenLimitcarries aperiod(0 = one-time, non-zero = recurring in seconds). - If
allowAnyCallsisfalse, stores theallowedCallsallowlist inkeyScopes.allowAnyCalls = falsewithallowedCalls = []means scoped deny-all. - Recipient-constrained selector rules are validated before any state is written.
- Emits
KeyAuthorizedevent
Requirements:
- MUST be called by the Root Key or an active admin access key
- MUST be invoked via the canonical selector
0x980a6025(the(address,uint8,(uint64,bool,(address,uint256,uint64)[],bool,(address,(bytes4,address[])[])[]))shape). Other selectors revert. keyIdMUST NOT beaddress(0)(reverts withZeroPublicKey)keyIdMUST NOT already be authorized withexpiry > 0(reverts withKeyAlreadyExists)keyIdMUST NOT have been previously revoked (reverts withKeyAlreadyRevoked- prevents replay attacks)signatureTypeMUST be0(Secp256k1),1(P256), or2(WebAuthn) (reverts withInvalidSignatureType)config.expiryMUST be strictly greater than the current block timestamp (reverts withExpiryInPast)- To authorize a non-expiring key, omit
key_authorization.expiryin the transaction RLP or passu64::MAXwhen calling the precompile ABI directly. Do not pass0. enforceLimitsdetermines whether spending limits are enforced for this keylimitsare only processed ifenforceLimitsistrue. Duplicate token entries revert withInvalidSpendingLimit.- Invalid call-scope shapes (zero targets, duplicate targets, duplicate selectors, duplicate recipients, malformed recipient-bound rules) revert with
InvalidCallScope.
Admin Key Authorization
- Creates a new key entry with
is_admin = true. - Emits the existing
KeyAuthorizedevent and the additionalAdminKeyAuthorizedevent. - Burns the provided
witnessfor replay protection.bytes32(0)is valid. - Admin keys can authorize and revoke keys, including other admin keys.
- Admin keys cannot carry expiry, spending limits, or call scopes.
Requirements:
- MUST be called by the Root Key or an active admin access key.
keyIdMUST NOT be the account address (reverts withInvalidKeyId).keyIdMUST NOT already be authorized (reverts withKeyAlreadyExists).keyIdMUST NOT have been previously revoked (reverts withKeyAlreadyRevoked).signatureTypeMUST be0(Secp256k1),1(P256), or2(WebAuthn) (reverts withInvalidSignatureType).
Key Revocation
- Marks the key as revoked by setting
isRevokedtotrueandexpiryto0 - Once revoked, a
keyIdcan NEVER be re-authorized for this account (prevents replay attacks) - Any stored call-scope and periodic-limit state becomes inaccessible.
getAllowedCalls(...)returns scoped deny-all (isScoped = true, scopes = []) for revoked keys. - Key can no longer be used for transactions
- Emits
KeyRevokedevent
Requirements:
- MUST be called by the Root Key or an active admin access key
keyIdMUST exist (key withexpiry > 0) (reverts withKeyNotFoundif not found)
Spending Limit Update
- Updates the spending limit for a specific token on an authorized key
- Allows the Root Key or an active admin access key to modify limits without revoking and re-authorizing the key
- If the key had unlimited spending (
enforceLimits == false), enables limits - Sets both
remainingandmaxtonewLimit. The configuredperiodand currentperiodEndare preserved. newLimitMUST fit within TIP20'su128supply range.- Emits
SpendingLimitUpdatedevent
Requirements:
- MUST be called by the Root Key or an active admin access key
keyIdMUST exist and not be revoked (reverts withKeyNotFoundorKeyAlreadyRevoked)keyIdMUST not be expired (reverts withKeyExpired)keyIdMUST not be an admin access key, because admin keys do not carry spending limits (reverts withInvalidKeyId)
Allowed Call Updates
setAllowedCalls(...)creates or replaces one or more target scopes for an existing key.removeAllowedCalls(...)removes one stored target scope.- An empty
selectorRulesarray means any selector on that target is allowed. setAllowedCalls(...)rejects an empty scope batch, zero targets, duplicate targets, duplicate selectors, duplicate recipients, and invalid recipient-constrained rules (reverts withInvalidCallScope).
Requirements:
- MUST be called by the Root Key or an active admin access key
keyIdMUST exist and not be revokedkeyIdMUST not be an admin access key, because admin keys do not carry call scopes (reverts withInvalidKeyId)
View Behavior
getKey(...)returns key metadata.getRemainingLimitWithPeriod(...)returns the effectiveremainingamount and currentperiodEndfor a key-token pair, accounting for periodic rollover.getAllowedCalls(...)returns(isScoped, scopes). Unrestricted keys returnisScoped = false. Scoped keys returnisScoped = truewith theirCallScope[]. Missing, revoked, or expired access keys returnisScoped = true, scopes = [](scoped deny-all).isAdminKey(...)returnstruefor the Root Key and for active, non-revoked, non-expired admin access keys.getTransactionKey()returns the key used in the current transaction.address(0)means the Root Key.- Missing, revoked, or expired keys return zeroed limit values.
Security Considerations
Access Key Storage
Access Keys should be securely stored to prevent unauthorized access. Call scopes make per-app and per-device key isolation more important, because a mis-scoped key may have a broader allowlist than intended.
- Device and Application Scoping: Access Keys SHOULD be scoped to a specific client device AND application combination. Access Keys SHOULD NOT be shared between devices or applications, even if they belong to the same user.
- 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: falsewhen generating Keys in web browsers. - 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.
Privilege Escalation Prevention
Limited access keys cannot escalate their own privileges because:
- Management functions (
authorizeKey,authorizeAdminKey,revokeKey,updateSpendingLimit,setAllowedCalls,removeAllowedCalls) are restricted to Root Key or active admin-key transactions - The protocol sets
transactionKey[account]during transaction validation to indicate which key signed the transaction - These management functions check that the caller is the Root Key or an active admin key before executing
- Mutable precompile calls also require
msg.sender == tx.origin, which prevents contract-mediated confused-deputy patterns - Limited access keys cannot bypass these checks - transactions will revert with
UnauthorizedCaller
Spending Limit Enforcement
- Spending limits are only enforced if
enforceLimits == truefor the key - Keys with
enforceLimits == falsehave unlimited spending (no limits checked) - Spending limits are enforced by the protocol internally calling
verify_and_update_spending()during execution - Limits are per-TIP20 token and deplete as TIP20 tokens are spent
- Recurring limits (
period > 0) roll over tomaxwhencurrent_timestamp >= periodEnd. Callers can observe rollover state viagetRemainingLimitWithPeriod(...). - Spending limits only track TIP20 token transfers (via
transferandtransferWithMemo) and approvals (viaapprove) - For approvals: only increases in approval amount count against the spending limit. This means approvals indirectly control
transferFromspending, sincetransferFromrequires a prior approval - Non-TIP20 asset movements (ETH, NFTs) are not subject to spending limits
- Root keys (
keyId == address(0)) have no spending limits - the function returns immediately - Missing, revoked, or expired keys have an effective remaining limit of zero
- Failed limit checks revert the entire transaction with
SpendingLimitExceeded
Call Scope Enforcement
- 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. - Access-key-signed transactions cannot create contracts in any configuration — including direct
CREATE, factoryCREATE, and internalCREATE2. Only Root-Key-signed transactions may perform contract creation.
Key Expiry
- Keys with
expiry > 0are checked against the current timestamp during validation - Expired keys cause transaction rejection with
KeyExpirederror (checked viavalidate_keychain_authorization()) - New authorizations require a future expiry timestamp
- Expiry is checked as:
current_timestamp >= expiry(key is expired when current time reaches or exceeds expiry) - Expired keys return zeroed limit and call-scope reads.
Usage Patterns
First-Time Access Key Authorization
- User signs Passkey prompt → signs over
key_authorizationfor a new Access Key (e.g., WebCrypto P256 key). The signed authorization carriesKeyRestrictions, allowing the same first-use flow to provision recurring limits and call scopes. - User's Access Key signs the transaction
- Transaction includes the
key_authorizationAND the Access Keysignature - Protocol validates Passkey signature on
key_authorization, setstransactionKey[account] = 0, callsAccountKeychain.authorizeKey(), then validates Access Key signature - Transaction executes with Access Key's spending limits enforced via internal
verify_and_update_spending(), plus call-scope checks if the key is scoped
Subsequent Access Key Usage
- User's Access Key signs the transaction (no
key_authorizationneeded) - Protocol validates the Access Key via
validate_keychain_authorization(), setstransactionKey[account] = keyId - Transaction executes with spending limit enforcement via internal
verify_and_update_spending()and call-scope enforcement. Contract creation is rejected.
Root Key or Admin Key Revoking or Updating an Access Key
- User signs with the Root Key or an active admin key → signs transaction calling
revokeKey(keyId),updateSpendingLimit(...),setAllowedCalls(...), orremoveAllowedCalls(...) - Transaction executes, marking the Access Key as inactive or updating its restrictions
- Future transactions signed by that Access Key are rejected (after revocation) or evaluated against the updated restrictions
Was this helpful?