Skip to content
LogoLogo

TIP-1011: Enhanced Access Key Permissions

Abstract

This TIP extends Access Keys with two new permission features: (1) periodic spending limits that automatically reset after a configurable time period, enabling subscription-based access patterns, and (2) destination address scoping that restricts keys to only interact with specific contract addresses.

Motivation

Currently, Access Keys support spending limits (per-TIP-20 token caps) and expiry timestamps. However, these primitives are insufficient for common real-world patterns:

Periodic Spending Limits

The existing TokenLimit specifies a one-time spending cap that depletes permanently. This model doesn't support subscription-based patterns where:

  • A service needs recurring access to a fixed amount per billing cycle
  • Users want to authorize "up to X tokens per month" without re-authorizing
  • dApps implement subscription models (e.g., streaming services, SaaS payments)

Use cases:

  1. Subscription services: Authorize a streaming service to charge 10 USDC/month
  2. Recurring donations: Allow an NPO to withdraw up to 5 USDC weekly
  3. Payroll systems: Enable payroll contracts to transfer salaries monthly
  4. Rate-limited APIs: Authorize API access keys with per-period token budgets

Destination Address Scoping

Users have requested the ability to bind Access Keys to specific destinations (e.g., "only allow transactions to Uniswap"). This provides more granular permission scoping similar to Solana's delegate primitive.

Use cases:

  1. DeFi integrations: Allow a trading bot key to only interact with specific DEX contracts
  2. Gaming: Scope a session key to only interact with a game contract
  3. Subscription services: Allow a key to only call a specific payment contract

Current workaround: Deploy a proxy contract that enforces destination restrictions, adding gas overhead and complexity.


Specification

Extended Data Structures

TokenLimit

Current:

struct TokenLimit {
    address token;
    uint256 limit;
}

Proposed:

struct TokenLimit {
    address token;
    uint256 limit;      // Per-period limit when period > 0, one-time limit otherwise
    uint256 remainingInPeriod;  // Remaining allowance in current period
    uint64 period;      // Period duration in seconds (0 = one-time limit)
    uint64 periodEnd;   // Timestamp when current period expires
}

KeyAuthorization

Current fields retained:

  • spendingLimits: TokenLimit[]
  • expiry: uint64

New field:

struct KeyAuthorization {
    TokenLimit[] spendingLimits;
    uint64 expiry;
    address[] allowedDestinations; // Empty array = unrestricted
}

Interface Changes

IAccountKeychain.sol

/// @notice Authorizes a key with enhanced permissions
/// @param key The public key to authorize
/// @param expiry Block timestamp when key expires
/// @param spendingLimits Token spending limits (may include periodic limits)
/// @param allowedDestinations Addresses the key may call (empty = unrestricted)
function authorizeKey(
    bytes calldata key,
    uint64 expiry,
    TokenLimit[] calldata spendingLimits,
    address[] calldata allowedDestinations
) external;
 
/// @notice Returns the allowed destinations for a key
/// @param key The public key to query
/// @return destinations Array of allowed destination addresses (empty = unrestricted)
function getAllowedDestinations(bytes calldata key) external view returns (address[] memory destinations);
 
/// @notice Returns the remaining limit for a token, accounting for period resets
/// @param key The public key to query
/// @param token The token address
/// @return remaining The remaining spending limit for the current period
/// @return periodEnd The timestamp when the current period ends (0 if one-time limit)
function getRemainingLimit(bytes calldata key, address token) external view returns (uint256 remaining, uint64 periodEnd);

Semantic Behavior

Periodic Limit Reset Logic

When a spending attempt occurs:

function verifyAndUpdateSpending(key, token, amount):
    limit = getTokenLimit(key, token)
    
    if limit.period > 0:  // Periodic limit
        if block.timestamp >= limit.periodEnd:
            // Reset period
            limit.periodEnd = block.timestamp + limit.period
            limit.remainingInPeriod = limit.limit  // Reset to per-period allowance
    
    if amount > limit.remainingInPeriod:
        revert SpendingLimitExceeded()
    
    limit.remainingInPeriod -= amount

Destination Validation Logic

When a transaction is submitted with an Access Key:

function validateDestination(key, destination):
    allowed = getAllowedDestinations(key)
    
    if allowed.length == 0:
        return true  // Unrestricted
    
    for addr in allowed:
        if addr == destination:
            return true
    
    revert DestinationNotAllowed()

Interaction Rules

  1. Mixed limits: Keys can have a mix of one-time and periodic limits for different tokens
  2. Destination + limits: Both constraints are evaluated independently; both must pass
  3. Period updates: Calling updateSpendingLimit() updates the per-period amount and resets the current period
  4. Empty destinations: An empty allowedDestinations array means the key is unrestricted (can call any address)

Gas Costs

OperationAdditional Gas
authorizeKey with N destinations~20,000 + 5,000 × N
authorizeKey with periodic limit~3,000 per periodic token
Destination check (per tx)~2,100 + 200 × N
Period reset (when triggered)~5,000

Encoding

Transaction Authorization

The KeyAuthorization struct is RLP-encoded in the transaction's authorization field:

KeyAuthorization := RLP([
    spendingLimits: [TokenLimit, ...],
    expiry: uint64,
    allowedDestinations: [address, ...]
])

TokenLimit := RLP([
    token: address,
    limit: uint256,
    remainingInPeriod: uint256,
    period: uint64,
    periodEnd: uint64
])

Backward Compatibility

This TIP requires a hardfork due to changes in transaction encoding and execution semantics.

RLP Encoding Changes

TokenLimit (2 fields → 5 fields)

The current TokenLimit struct encodes as [token, limit]. This TIP extends it to [token, limit, remainingInPeriod, period, periodEnd].

Breaking change: Old nodes cannot decode new transactions with 5-field TokenLimit. New nodes must implement version-tolerant decoding:

On decode:
  if list.len() == 2:
    // V1 (legacy one-time limit)
    remainingInPeriod = limit
    period = 0
    periodEnd = 0
  else if list.len() == 5:
    // V2 (periodic limit)
    decode all fields
  else:
    error

Post-fork, all new TokenLimit encodings MUST use the 5-field format for consistency.

KeyAuthorization (trailing field addition)

KeyAuthorization uses #[rlp(trailing)] which allows appending optional fields. Adding allowedDestinations: Option<Vec<Address>> as the last field is compatible with this pattern:

  • Old encodings (without allowedDestinations) decode as allowedDestinations = None (unrestricted)
  • New encodings with allowedDestinations will be rejected by old nodes

Compact/Database Encoding

Although TokenLimit has #[derive(reth_codecs::Compact)], it is not used in production storage. The storage path is:

TempoTransaction (derived Compact)
  └─ key_authorization: Option<SignedKeyAuthorization>
       └─ SignedKeyAuthorization (custom Compact impl → uses RLP internally)
            └─ KeyAuthorization (RLP encoded)
                 └─ limits: Option<Vec<TokenLimit>> (RLP encoded)

SignedKeyAuthorization implements a custom Compact that wraps RLP encoding. Therefore, TokenLimit is always serialized as RLP bytes in the database, not via its derived Compact layout.

Implication: If we implement version-tolerant RLP decoding for TokenLimit (accept 2-field or 5-field lists), existing database entries will decode correctly. No DB rebuild required for this change.

Precompile Storage Changes

The current storage layout:

  • keys[account][keyId] → AuthorizedKey (packed slot)
  • spending_limits[key][token] → U256 (remaining amount)

This TIP requires additional per-token state for periodic limits. Additive storage approach (no migration required):

MappingTypeDescription
spending_limits[key][token]U256Reinterpreted as remainingInPeriod
spending_limit_max[key][token]U256Per-period cap (new)
spending_limit_period[key][token]u64Period duration in seconds (new)
spending_limit_period_end[key][token]u64Current period end timestamp (new)

For destination scoping:

MappingTypeDescription
allowed_destinations_len[key]u64Number of allowed destinations
allowed_destinations[key][index]AddressAllowed destination at index

Legacy keys (pre-fork) have period = 0, max = 0, and behave as one-time limits.

Hardfork-Gated Features

The following MUST be gated behind the hardfork activation:

  1. RLP decoding: Accept 5-field TokenLimit and allowedDestinations in KeyAuthorization
  2. Periodic limit reset logic: Check periodEnd and reset remainingInPeriod on spend
  3. Destination scoping enforcement: Validate transaction to against allowedDestinations
  4. New precompile storage writes: Write to new storage slots for period/destination data
  5. New precompile interface methods: getAllowedDestinations(), updated getRemainingLimit() return type

Pre-fork blocks MUST be replayed with old semantics to preserve state root consistency.


Invariants

  1. Period monotonicity: periodEnd MUST only increase; it cannot be set to a past timestamp.

  2. Limit conservation: For periodic limits, remainingInPeriod MUST NOT exceed limit after any reset.

  3. Destination enforcement: If allowedDestinations is non-empty, transactions to addresses not in the list MUST revert.

  4. Backward compatibility: Keys authorized without the new fields MUST behave as unrestricted (allowedDestinations = []) with one-time limits (period = 0).

  5. Expiry precedence: Key expiry MUST be checked before spending limits or destination restrictions.

Test Cases

  1. Periodic reset: Verify that a periodic limit resets correctly after the period elapses
  2. Partial period usage: Verify that unused periodic allowance does not roll over
  3. Destination allow: Verify that transactions to allowed destinations succeed
  4. Destination deny: Verify that transactions to non-allowed destinations revert
  5. Empty destinations: Verify that empty allowedDestinations allows any destination
  6. Mixed limits: Verify that a key can have both one-time and periodic limits for different tokens
  7. Upgrade path: Verify that existing keys continue to function after upgrade

References