---
id: TIP-1049
title: Admin Access Keys
description: Extend access keys with an admin flag that grants key-management capabilities
authors: Jake Moxey (@jxom), Georgios Konstantopoulos (@gakonst), Tanishk Goyal (@tanishkgoyal)
status: Approved
related: TIP-1011, TIP-1020
protocolVersion: T6
related: TIP-1011, TIP-1020
---

# TIP-1049: Admin Access Keys

## Abstract

This TIP adds an `admin` boolean flag to the `AuthorizedKey` storage struct. When `admin` is `true`, the access key may authorize and revoke other access keys on behalf of the account. The TIP also extends TIP-1020's `SignatureVerifier` precompile with stateful verification methods so external contracts can validate active and root/admin keychain signatures against the AccountKeychain state.

## Motivation

Access keys today are strictly subordinate: they can sign transactions within their spending limits and call scopes, but they cannot authorize new keys or participate in onchain key-management flows. This creates two practical problems:

1. **Cross-device key management.** A user with a passkey on their phone cannot authorize a second passkey on their laptop without a transaction from the root EOA key. In practice, multi-device setups require the root key to be available for every new device onboarding, which is inconvenient and fragile.

2. **Onchain admin/keychain signature verification.** Contracts that want to gate actions on "the root key or an admin access key of account X signed this" (e.g. ERC-1271-style flows), or validate that a keychain signature came from an active key, have no canonical primitive: today they would have to combine raw signature recovery with their own key-status lookups. Stateful verification methods that compose recovery with key-status checks remove that footgun.

Adding an `admin` flag plus stateful verification methods on the TIP-1020 `SignatureVerifier` precompile solves both problems with minimal surface area.

## Assumptions

- The root EOA key is implicitly admin. The `admin` flag only applies to access keys registered in the AccountKeychain precompile.
- Admin keys MUST NOT carry spending limits, call scopes, or expiry. Authorizations that combine `admin = true` with non-default `enforceLimits`, `allowedCalls`, or `expiry` MUST be rejected.
- TIP-1020 (`SignatureVerifier`) is extended with stateful keychain verification methods. `verifyKeychain` validates that a keychain signature was produced by an active access key for the expected account. `verifyKeychainAdmin` validates that a keychain signature was produced by the expected account's root key or an active admin access key.

---

# Specification

## Storage Layout

The `AuthorizedKey` packed struct gains a new `is_admin` boolean at byte 11:

```
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)        ← NEW
```

This adds one byte to the existing packed slot. No new storage slots are introduced for the admin flag.

## Interface Changes

### `authorizeAdminKey`

A new function on the `AccountKeychain` precompile authorizes an admin key for the caller's account.

```solidity
/// @notice Authorizes an admin key for the caller's account.
/// @param keyId The key identifier (address derived from public key)
/// @param signatureType 0: secp256k1, 1: P256, 2: WebAuthn
/// @param witness TIP-1053 key-authorization witness for this authorization
function authorizeAdminKey(
    address keyId,
    SignatureType signatureType,
    bytes32 witness
) external;
```

- Guarded by `ensure_admin_caller`.
- MUST reject if `witness` is already burned for `account`. `bytes32(0)` is a valid witness value.
- MUST reject `keyId == account` with `AccountKeychainError::InvalidKeyId`, including attempts to authorize the root EOA key as an admin access key.
- MUST reject if `keys[account][keyId]` is already registered with `AccountKeychainError::KeyAlreadyExists`; previously revoked key IDs MUST reject with `AccountKeychainError::KeyAlreadyRevoked`. Only newly authorized keys can be marked as admin.
- On success, sets `keys[account][keyId].is_admin = true` and emits `AdminKeyAuthorized`. It also emits the TIP-1053 witness event for `witness`.

### `AdminKeyAuthorized` Event

The existing `KeyAuthorized` event is **unchanged** to preserve backward compatibility for indexers and decoders. A new event is added and emitted **alongside** `KeyAuthorized` whenever `admin = true`:

```solidity
event AdminKeyAuthorized(
    address indexed account,
    address indexed publicKey
);
```

### `SignatureVerifier` keychain/admin verification

TIP-1020's `SignatureVerifier` precompile is extended with stateful verification helpers:

```solidity
/// @notice Returns true if `signature` over `digest` is a valid keychain
/// signature produced by an active access key on `account`. Returns false
/// for account mismatches, unknown, revoked, or expired access keys. Reverts
/// on malformed non-keychain signatures and invalid keychain signatures.
/// @dev Does not compare the inner signature type against the stored key type.
function verifyKeychain(
    address account,
    bytes32 digest,
    bytes calldata signature
) external view returns (bool);

/// @notice Returns true if `signature` over `digest` is a valid keychain
/// signature produced by the root key or an active admin access key on
/// `account`. Returns false for account mismatches, non-admin, unknown,
/// revoked, or expired access keys. Reverts on malformed non-keychain
/// signatures and invalid keychain signatures.
/// @dev Does not compare the inner signature type against the stored key type.
function verifyKeychainAdmin(
    address account,
    bytes32 digest,
    bytes calldata signature
) external view returns (bool);
```

`verifyKeychain` and `verifyKeychainAdmin` only accept V2 keychain signatures. They recover the embedded root account and access-key signer using the keychain signature rules, then require the embedded root account to equal `account`. `verifyKeychain` returns true when the recovered access key is active for `account`. `verifyKeychainAdmin` returns true when the recovered signer is `account` itself or an active admin access key for `account`.

**Note:** These methods do not compare the inner signature type against the key type stored in AccountKeychain. This avoids an additional storage read; callers that need to enforce the stored signature type must perform that check separately.

Both methods propagate signature parsing and recovery errors using the existing `SignatureVerifier.InvalidFormat` and `SignatureVerifier.InvalidSignature` errors. Contracts that need non-reverting behavior SHOULD use Solidity `try`/`catch` and treat failures as invalid signatures.

The existing stateless `verify(signer, digest, sig)` is unchanged.

### `AccountKeychain.isAdminKey`

A single read-only helper is added to the AccountKeychain precompile:

```solidity
/// Returns true if `keyId` is currently an authorized, non-revoked,
/// non-expired key on `account` with `is_admin = true`. Returns true
/// when `keyId == account` (root key).
function isAdminKey(address account, address keyId) external view returns (bool);
```

Contracts that need to inspect admin status outside a signature-verification flow (e.g. permissioned hooks) call this directly.

## Behavioral Changes

### Admin-Gated Keychain Mutators

`ensure_admin_caller` is widened to additionally accept admin access keys. Every mutator gated by `ensure_admin_caller` (including the new `authorizeAdminKey`) MAY now be called by either the root key or an admin access key, and MUST revert with `AccountKeychainError::UnauthorizedCaller` for any other caller.

`authorizeAdminKey` MUST reject `keyId == account` with `AccountKeychainError::InvalidKeyId`.

For stored access-key rows, mutators operate on the row selected by `keyId`:

- When the selected row is an admin key, `updateSpendingLimit`, `setAllowedCalls`, and `removeAllowedCalls` MUST reject with `AccountKeychainError::InvalidKeyId`. Admin keys have no `KeyRestrictions`.
- `revokeKey` MAY be called and revokes any authorized key, including an admin key targeting itself.

### TIP-1020 Compatibility

TIP-1020's existing stateless `verify(signer, digest, sig)` is unchanged. This TIP only **adds** stateful admin/keychain verification methods to the same `SignatureVerifier` precompile. Existing callers of `verify` are unaffected.

### Admin Key Privilege Propagation

An admin access key MAY authorize new keys (admin or non-admin) by signing the `KeyAuthorization` itself, enabling delegation chains. The same admin key MUST also sign the transaction carrying that `KeyAuthorization`. This is intentional: it allows multi-device setups where any trusted device can onboard new devices without the root key being available at all.

Revocation of an admin key does NOT automatically revoke keys that it authorized. Each key is independently tracked; cascading revocation is out of scope for this TIP. Accounts that need cascading revocation should revoke downstream keys explicitly.

## Transaction Encoding

`KeyAuthorization` remains an RLP list. TIP-1053 adds `witness?`; TIP-1049 adds the `is_admin?` and `account?` trailing optional fields after it:

```
rlp([chain_id, key_type, key_id, expiry?, limits?, allowed_calls?, witness?, is_admin?, account?])
```

- If `is_admin` is omitted or `false`, the authorization creates a non-admin key.
- If `is_admin` is `true`, the authorization creates an admin key.
- If the authorization signer is not the target account's root key, `account` MUST be present and equal the target account being modified.
- If the authorization signer is the target account's root key, the transaction MUST be signed by the root key, except for same-transaction authorize-and-use where the transaction key is the key being authorized.
- If the authorization signer is an admin access key, the transaction MUST be signed by that same admin access key. A transaction signed by the root key or a different admin access key MUST NOT carry an admin-signed `KeyAuthorization`.
- If `account` is present, it MUST equal the target account being modified.
- If `is_admin` is `true`, `expiry`, `limits`, and `allowed_calls` MUST be omitted or encoded as empty optional values because admin keys carry no restrictions.
- If `account` is present, `witness` MAY be present or omitted for the transaction-encoded authorization. ABI calls to `authorizeAdminKey` always provide a `bytes32 witness`, including `bytes32(0)`.

In root/admin terms:

- Root key authorizes non-admin key: `account` MAY be omitted. If present, it MUST equal the target account. The transaction MUST be signed by the root key, except for same-transaction authorize-and-use by the key being authorized.
- Root key authorizes admin key: `account` MAY be omitted. If present, it MUST equal the target account. The transaction MUST be signed by the root key, except for same-transaction authorize-and-use by the key being authorized.
- Admin key authorizes non-admin key: `account` MUST be present, MUST equal the target account, and the transaction MUST be signed by the same admin key.
- Admin key authorizes admin key: `account` MUST be present, MUST equal the target account, and the transaction MUST be signed by the same admin key.
- Non-admin access key authorizes any key: invalid.

The target `account`, `is_admin`, and `witness` fields are part of the signed RLP payload. This binds admin-signed key authorizations to a target account and prevents replaying an unrestricted non-admin authorization as an admin authorization.

# Invariants

1. **Root-equivalence for admin keys.** Admin access keys MUST be accepted by `ensure_admin_caller` for all six admin-gated keychain mutators (`authorizeKey`, `authorizeAdminKey`, `revokeKey`, `updateSpendingLimit`, `setAllowedCalls`, `removeAllowedCalls`).

2. **TIP-1020 stateless methods unchanged.** TIP-1020's `recover` and `verify` MUST behave identically to pre-TIP-1049 semantics.

3. **Backward compatibility.** Existing access keys (authorized before this TIP activates) default to `is_admin = false`. Their behavior is unchanged.
